Skip to content

Commit d5f7098

Browse files
committed
Merge branch 'store-build-tags'
After the recent optimization work, I wanted to experiment with using build tags to gate which store backends are included in a randomizer binary. This _might_ be useful for my Cloud Run instance if I build a Firestore-only version of the standard container image.
2 parents 80e11f7 + 2fcfc10 commit d5f7098

File tree

10 files changed

+161
-19
lines changed

10 files changed

+161
-19
lines changed

SERVERMORE.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,34 @@ token (the newer signing secret configuration isn't supported):
3535

3636
## Storage Backends
3737

38+
By default, the `randomizer-server` build supports all of the following storage
39+
backends, and permits you to select between them using environment variables.
40+
If you must optimize your build time or binary size, you can run `go build`
41+
with `-tags` listing the comma-separated build tags of the backends you wish to
42+
support (e.g. `go build -tags=randomizer.bbolt,randomizer.dynamodb …`).
43+
3844
### bbolt
3945

46+
`-tags=randomizer.bbolt`
47+
4048
[bbolt][bbolt] is the local key-value database engine behind systems like etcd,
4149
Consul, InfluxDB 2.x, and more.
4250

4351
The bbolt backend's only prerequisite is persistent disk storage. Since a
4452
single running server locks the database file, this backend won't support high
4553
availability or zero-downtime deployment.
4654

47-
The bbolt backend is active by default if no other backends have environment
48-
variables set. You can set `DB_PATH` to the desired location of the database on
49-
disk, otherwise it defaults to "randomizer.db" in the current directory.
55+
To activate the bbolt backend, set `DB_PATH` to the desired location of the
56+
database on disk. If you haven't used build tags to customize your server's
57+
supported backends, and no other store is configured, the server activates the
58+
bbolt backend by default as if `DB_PATH=randomizer.db` had been set.
5059

5160
[bbolt]: https://go.etcd.io/bbolt
5261

5362
### DynamoDB
5463

64+
`-tags=randomizer.dynamodb`
65+
5566
DynamoDB is Amazon's fully managed NoSQL key-value store.
5667

5768
The DynamoDB backend requires a pre-existing table with the randomizer schema.
@@ -67,6 +78,8 @@ in the code are unstable, and are subject to removal or behavior changes.)
6778

6879
### Google Cloud Firestore
6980

81+
`-tags=randomizer.firestore`
82+
7083
Firestore is Google's fully managed document database. This mode is especially
7184
useful to run `randomizer-server` on [Cloud Run][Cloud Run], with a level of
7285
operational ease comparable to the AWS Lambda solution (though the randomizer

internal/store/bbolt/factory.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package bbolt
22

33
import (
4+
"context"
45
"os"
56

67
bolt "go.etcd.io/bbolt"
78

89
"github.com/ahamlinman/randomizer/internal/randomizer"
10+
"github.com/ahamlinman/randomizer/internal/store/registry"
911
)
1012

13+
func init() {
14+
registry.Provide("bbolt", FactoryFromEnv, "DB_PATH")
15+
}
16+
1117
// FactoryFromEnv returns a store.Factory whose stores are backed by a local
1218
// Bolt database (using the CoreOS "bbolt" fork).
13-
func FactoryFromEnv() (func(string) randomizer.Store, error) {
19+
func FactoryFromEnv(_ context.Context) (func(string) randomizer.Store, error) {
1420
path := pathFromEnv()
1521

1622
db, err := bolt.Open(path, os.ModePerm&0644, nil)
@@ -31,6 +37,5 @@ func pathFromEnv() string {
3137
if path := os.Getenv("DB_PATH"); path != "" {
3238
return path
3339
}
34-
3540
return "randomizer.db"
3641
}

internal/store/dynamodb/factory.go

+6
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ import (
99

1010
"github.com/ahamlinman/randomizer/internal/awsconfig"
1111
"github.com/ahamlinman/randomizer/internal/randomizer"
12+
"github.com/ahamlinman/randomizer/internal/store/registry"
1213
)
1314

15+
func init() {
16+
registry.Provide("dynamodb", FactoryFromEnv,
17+
"DYNAMODB", "DYNAMODB_TABLE", "DYNAMODB_ENDPOINT")
18+
}
19+
1420
// FactoryFromEnv returns a store.Factory whose stores are backed by Amazon
1521
// DynamoDB.
1622
//

internal/store/firestore/factory.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ import (
88
"cloud.google.com/go/firestore"
99

1010
"github.com/ahamlinman/randomizer/internal/randomizer"
11+
"github.com/ahamlinman/randomizer/internal/store/registry"
1112
)
1213

14+
func init() {
15+
registry.Provide("firestore", FactoryFromEnv,
16+
"FIRESTORE_PROJECT_ID", "FIRESTORE_DATABASE_ID")
17+
}
18+
1319
// FactoryFromEnv returns a store.Factory whose stores are backed by a Google
1420
// Cloud Firestore database.
15-
func FactoryFromEnv() (func(string) randomizer.Store, error) {
21+
func FactoryFromEnv(_ context.Context) (func(string) randomizer.Store, error) {
1622
projectID, ok := os.LookupEnv("FIRESTORE_PROJECT_ID")
1723
if !ok {
1824
return nil, errors.New("missing FIRESTORE_PROJECT_ID in environment")

internal/store/registry/registry.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Package registry facilitates environment-based store setup without requiring
2+
// every store to be linked into a randomizer binary.
3+
package registry
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/ahamlinman/randomizer/internal/randomizer"
10+
)
11+
12+
// Registry provides environment keys and factory constructors for the store
13+
// backends linked into this binary.
14+
var Registry = map[string]Entry{}
15+
16+
// Entry represents a single store backend.
17+
type Entry struct {
18+
// EnvironmentKeys is the list of environment variables that this store's
19+
// factory checks for configuration. If any one of these keys is set in the
20+
// environment (and no conflicting keys are set), this store will be selected
21+
// as the backend for this randomizer instance.
22+
EnvironmentKeys []string
23+
24+
// FactoryFromEnv creates a factory for this backend based on its environment
25+
// variables.
26+
FactoryFromEnv func(context.Context) (func(partition string) randomizer.Store, error)
27+
}
28+
29+
// Provide registers a new store backend, or panics if a backend is already
30+
// registered under this name.
31+
func Provide(
32+
name string,
33+
factoryFromEnv func(context.Context) (func(partition string) randomizer.Store, error),
34+
environmentKeys ...string,
35+
) {
36+
if _, ok := Registry[name]; ok {
37+
panic(fmt.Errorf("%s already registered as a store backend", name))
38+
}
39+
Registry[name] = Entry{
40+
EnvironmentKeys: environmentKeys,
41+
FactoryFromEnv: factoryFromEnv,
42+
}
43+
}

internal/store/store.go

+54-13
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,75 @@ package store
44

55
import (
66
"context"
7+
"errors"
8+
"fmt"
9+
"maps"
710
"os"
11+
"slices"
812

913
"github.com/ahamlinman/randomizer/internal/randomizer"
10-
"github.com/ahamlinman/randomizer/internal/store/bbolt"
11-
"github.com/ahamlinman/randomizer/internal/store/dynamodb"
12-
"github.com/ahamlinman/randomizer/internal/store/firestore"
14+
"github.com/ahamlinman/randomizer/internal/store/registry"
1315
)
1416

17+
// haveAllStoreBackends indicates whether we can safely use the bbolt fallback
18+
// explained in the [FactoryFromEnv] comment. If we fall back to bbolt even
19+
// though we don't know the full set of possible environment keys (because we
20+
// used build tags to exclude some backends), we might activate it for a user
21+
// who meant to configure one of those missing stores instead.
22+
var haveAllStoreBackends bool
23+
1524
// Factory represents a type for functions that produce a store for the
1625
// randomizer to use for a given "partition" (e.g. Slack channel). Factories
1726
// may panic if a non-empty partition is required and not given.
1827
//
1928
// Factory is provided for documentation purposes. Do not import the store
20-
// package just to use this alias; this will link support for all possible
29+
// package just to use this alias; this may link support for all possible
2130
// store backends into the final program, even if this was not intended.
2231
type Factory = func(partition string) randomizer.Store
2332

24-
// FactoryFromEnv constructs and returns a [Factory] based on available
25-
// environment variables. If a known DynamoDB environment variable is set, it
26-
// will return a DynamoDB store. Otherwise, it will return a bbolt store.
27-
func FactoryFromEnv(ctx context.Context) (func(string) randomizer.Store, error) {
28-
if envHasAny("DYNAMODB", "DYNAMODB_TABLE", "DYNAMODB_ENDPOINT") {
29-
return dynamodb.FactoryFromEnv(ctx)
33+
// FactoryFromEnv constructs and returns a [Factory] based on runtime
34+
// environment variables, using one of the store backends included in the
35+
// binary based on Go build tags.
36+
//
37+
// Each store backend defines a set of environment variables for configuration.
38+
// On startup, the randomizer selects one store backend based on the presence
39+
// of its environment variables. It may fail if the environment has conflicting
40+
// or missing store configurations, or use a default bbolt configuration if no
41+
// build tags have been used to restrict the backends available in this binary.
42+
func FactoryFromEnv(ctx context.Context) (Factory, error) {
43+
if len(registry.Registry) == 0 {
44+
return nil, errors.New("no store backends available in this build")
45+
}
46+
47+
candidates := make(map[string]struct{})
48+
for name, entry := range registry.Registry {
49+
if envHasAny(entry.EnvironmentKeys...) {
50+
candidates[name] = struct{}{}
51+
}
52+
}
53+
54+
var chosen string
55+
if len(candidates) == 0 && haveAllStoreBackends {
56+
chosen = "bbolt"
57+
}
58+
if len(candidates) == 1 {
59+
for name := range candidates {
60+
chosen = name
61+
}
62+
}
63+
64+
if chosen == "" && len(candidates) == 0 {
65+
available := slices.Sorted(maps.Keys(registry.Registry))
66+
return nil, fmt.Errorf(
67+
"can't find environment settings to select between store backends: %v", available)
3068
}
31-
if envHasAny("FIRESTORE_PROJECT_ID", "FIRESTORE_DATABASE_ID") {
32-
return firestore.FactoryFromEnv()
69+
if chosen == "" {
70+
options := slices.Sorted(maps.Keys(candidates))
71+
return nil, fmt.Errorf(
72+
"environment settings match multiple store backends: %v", options)
3373
}
34-
return bbolt.FactoryFromEnv()
74+
75+
return registry.Registry[chosen].FactoryFromEnv(ctx)
3576
}
3677

3778
func envHasAny(names ...string) bool {

internal/store/store_all.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !randomizer.bbolt && !randomizer.dynamodb && !randomizer.firestore
2+
3+
package store
4+
5+
import (
6+
_ "github.com/ahamlinman/randomizer/internal/store/bbolt"
7+
_ "github.com/ahamlinman/randomizer/internal/store/dynamodb"
8+
_ "github.com/ahamlinman/randomizer/internal/store/firestore"
9+
)
10+
11+
func init() {
12+
haveAllStoreBackends = true
13+
}

internal/store/store_bbolt.go

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build randomizer.bbolt
2+
3+
package store
4+
5+
import _ "github.com/ahamlinman/randomizer/internal/store/bbolt"

internal/store/store_dynamodb.go

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build randomizer.dynamodb
2+
3+
package store
4+
5+
import _ "github.com/ahamlinman/randomizer/internal/store/dynamodb"

internal/store/store_firestore.go

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build randomizer.firestore
2+
3+
package store
4+
5+
import _ "github.com/ahamlinman/randomizer/internal/store/firestore"

0 commit comments

Comments
 (0)