summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_drafts/program-structure-and-composability.md161
1 files changed, 150 insertions, 11 deletions
diff --git a/_drafts/program-structure-and-composability.md b/_drafts/program-structure-and-composability.md
index 96baee8..7968405 100644
--- a/_drafts/program-structure-and-composability.md
+++ b/_drafts/program-structure-and-composability.md
@@ -6,6 +6,9 @@ description: >-
complex structures, and a pattern which helps in solving those problems.
---
+TODO:
+* Double check if I'm using "I" or "We" everywhere (probably should use "I")
+
## Part 0: Introduction
This post is focused on a concept I call "program structure", which I will try
@@ -113,9 +116,9 @@ looks something like:
```go
// A mapping of connection names to redis connections.
-var globalConns = map[string]redisConnection
+var globalConns = map[string]*RedisConn{}
-func Get(name string) redisConnection {
+func Get(name string) *RedisConn {
if globalConns[name] == nil {
globalConns[name] = makeConnection(name)
}
@@ -155,7 +158,7 @@ breaking compartmentalization. The person/team responsible for the central
library often finds themselves as the maintainers of the shared resource as
well, rather than the team actually using it.
-### Program Structure
+### Component Structure
So what does proper program structure look like? In my mind the structure of a
program is a hierarchy of components, or, in other words, a tree. The leaf nodes
@@ -179,19 +182,19 @@ TODO diagram:
http
```
-This structure contains the addition of the `debug` component. Clearly the
-`http` and `redis` components are reusable in different contexts, but for this
-example the `debug` endpoint is as well. It creates a separate http server which
-can be queried to perform runtime debugging of the program, and can be tacked
-onto virtually any program. The `rest-api` component is specific to this program
-and therefore not reusable. Let's dive into it a bit to see how it might be
-implemented:
+This component structure contains the addition of the `debug` component. Clearly
+the `http` and `redis` components are reusable in different contexts, but for
+this example the `debug` endpoint is as well. It creates a separate http server
+which can be queried to perform runtime debugging of the program, and can be
+tacked onto virtually any program. The `rest-api` component is specific to this
+program and therefore not reusable. Let's dive into it a bit to see how it might
+be implemented:
```go
// RestAPI is very much not thread-safe, hopefully it doesn't have to handle
// more than one request at once.
type RestAPI struct {
- redisConn *redis.Conn
+ redisConn *redis.RedisConn
httpSrv *http.Server
// Statistics exported for other components to see
@@ -265,4 +268,140 @@ discussed in the next section.
## Part 2: Context, Configuration, and Runtime
+The key to the configuration problem is to recognize that, even if there are two
+of the same component in a program, they can't occupy the same place in the
+program's structure. In the above example there are two `http` components, one
+under `rest-api` and the other under `debug`. Since the structure is represented
+as a tree of components, the "path" of any node in the tree uniquely represents
+it in the structure. For example, the two `http` components in the previous
+example have these paths:
+
+```
+root -> rest-api -> http
+root -> debug -> http
+```
+
+If each component were to know its place in the component tree, then it would
+easily be able to ensure that its configuration and initialization didn't
+conflict with other components of the same type. If the `http` component sets up
+a command-line parameter to know what address to listen on, the two `http`
+components in that program would set up:
+
+```
+--rest-api-listen-addr
+--debug-listen-addr
+```
+
+So how can we enable each component to know its path in the component structure?
+To answer this we'll have to take a detour through go's `Context` type.
+
+### Context and Configuration
+
+As I mentioned in the Introduction, my example language in this post is Go, but
+there's nothing about the concepts I'm presenting which are specific to Go. To
+put it simply, Go's builtin `context` package implements a type called
+`context.Context` which is, for all intents and purposes, an immutable key/value
+store. This means that when you set a key to a value on a Context (using the
+`context.WithValue` function) a new Context is returned. The new Context
+contains all of the original's key/values, plus the one just set. The original
+remains untouched.
+
+(Go's Context also has some behavior built into it surrounding deadlines and
+process cancellation, but those aren't relevant for this discussion.)
+
+Context makes sense to use for carrying information about the program's
+structure to it's different components; it is informing each of what _context_
+it exists in within the larger structure. To use Context effectively, however,
+it is necessary to implement some helper functions. Here are their function
+signatures:
+
+```go
+// NewChild creates and returns a new Context based off of the parent one. The
+// child will have a path which is the parent's path appended with the given
+// name.
+func NewChild(parent context.Context, name string) context.Context
+
+// Path returns the sequence of names which were used to produce this Context
+// via calls to the NewChild function.
+func Path(ctx context.Context) []string
+```
+
+`NewChild` is used to create a new Context, corresponding to a new child node in
+the component structure, and `Path` is used retrieve the path of any Context
+within that structure. For the sake of keeping the examples simple let's pretend
+these functions have been implemented in a package called `mctx`. Here's an
+example of how `mctx` might be used in the `redis` component's code:
+
+```go
+func NewRedis(ctx context.Context, defaultAddr string) *RedisConn {
+ ctx = mctx.NewChild(ctx, "redis")
+ ctxPath := mctx.Path(ctx)
+ paramPrefix := strings.Join(ctxPath, "-")
+
+ addrParam := flag.String(paramPrefix+"-addr", defaultAddr, "Address of redis instance to connect to")
+ // finish setup
+
+ return redisConn
+}
+```
+
+In our above example, the two `redis` components' parameters would be:
+
+```
+// This first parameter is for stats redis, whose parent is the root and
+// therefore doesn't have a prefix. Perhaps stats should be broken into its own
+// component in order to fix this.
+--redis-addr
+--rest-api-redis-addr
+```
+
+The prefix joining stuff will probably get annoying after a while though, so
+let's invent a new package, `mcfg`, which acts like `flag` but is aware of
+`mctx`. Then `NewRedis` is reduced to:
+
+```go
+func NewRedis(ctx context.Context, defaultAddr string) *RedisConn {
+ ctx = mctx.NewChild(ctx, "redis")
+ addrParam := flag.String(ctx, "-addr", defaultAddr, "Address of redis instance to connect to")
+ // finish setup
+
+ return redisConn
+}
+```
+
+Sharp-eyed gophers will notice that there's a key piece missing: When is
+`mcfg.Parse` called? When does `addrParam` actually get populated? Because you
+can't create the redis connection until that happens, but that can't happen
+inside `NewRedis` because there might be other things after `NewRedis` which
+want to set up parameters. To illustrate the problem, let's look at a simple
+program which wants to set up two `redis` components:
+
+```go
+func main() {
+ // Create the root context, and empty Context.
+ ctx := context.Background()
+
+ // Create the Contexts for two sub-components, foo and bar.
+ ctxFoo := mctx.NewChild(ctx, "foo")
+ ctxBar := mctx.NewChild(ctx, "bar")
+
+ // Now we want to try to create a redis instances for each component. But...
+
+ // This will set up the parameter "--foo-redis-addr", but bar hasn't had a
+ // chance to set up its corresponding parameter, so the command-line can't
+ // be parsed yet.
+ fooRedis := redis.NewRedis(ctxFoo, "127.0.0.1:6379")
+
+ // This will set up the parameter "--bar-redis-addr", but, as mentioned
+ // before, NewRedis can't parse command-line.
+ barRedis := redis.NewRedis(ctxBar, "127.0.0.1:6379")
+
+ // If the command-line is parsed here, then how can fooRedis and barRedis
+ // have been created yet? Creating the redis connection depends on the addr
+ // parameters having already been parsed and filled.
+}
+```
+
+We will solve this problem in the next section.
+## Init vs. Start