mctx: refactor so that contexts no longer carry mutable data
This change required refactoring nearly every package in this project, but it does a lot to simplify mctx and make other code using it easier to think about. Other code, such as mlog and mcfg, had to be slightly modified for this change to work as well.
This commit is contained in:
parent
0c2c49501e
commit
4b446a0efc
@ -12,6 +12,7 @@ package main
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@ -19,7 +20,6 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/m"
|
"github.com/mediocregopher/mediocre-go-lib/m"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcrypto"
|
"github.com/mediocregopher/mediocre-go-lib/mcrypto"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mhttp"
|
"github.com/mediocregopher/mediocre-go-lib/mhttp"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||||
@ -30,27 +30,26 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := m.NewServiceCtx()
|
ctx := m.NewServiceCtx()
|
||||||
logger := mlog.From(ctx)
|
ctx, cookieName := mcfg.String(ctx, "cookie-name", "_totp_proxy", "String to use as the name for cookies")
|
||||||
cookieName := mcfg.String(ctx, "cookie-name", "_totp_proxy", "String to use as the name for cookies")
|
ctx, cookieTimeout := mcfg.Duration(ctx, "cookie-timeout", mtime.Duration{1 * time.Hour}, "Timeout for cookies")
|
||||||
cookieTimeout := mcfg.Duration(ctx, "cookie-timeout", mtime.Duration{1 * time.Hour}, "Timeout for cookies")
|
|
||||||
|
|
||||||
var userSecrets map[string]string
|
var userSecrets map[string]string
|
||||||
mcfg.RequiredJSON(ctx, "users", &userSecrets, "JSON object which maps usernames to their TOTP secret strings")
|
ctx = mcfg.RequiredJSON(ctx, "users", &userSecrets, "JSON object which maps usernames to their TOTP secret strings")
|
||||||
|
|
||||||
var secret mcrypto.Secret
|
var secret mcrypto.Secret
|
||||||
secretStr := mcfg.String(ctx, "secret", "", "String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens.")
|
ctx, secretStr := mcfg.String(ctx, "secret", "", "String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens.")
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
||||||
if *secretStr == "" {
|
if *secretStr == "" {
|
||||||
*secretStr = mrand.Hex(32)
|
*secretStr = mrand.Hex(32)
|
||||||
}
|
}
|
||||||
logger.Info("generating secret")
|
mlog.Info(ctx, "generating secret")
|
||||||
secret = mcrypto.NewSecret([]byte(*secretStr))
|
secret = mcrypto.NewSecret([]byte(*secretStr))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
proxyHandler := new(struct{ http.Handler })
|
proxyHandler := new(struct{ http.Handler })
|
||||||
proxyURL := mcfg.RequiredString(ctx, "dst-url", "URL to proxy requests to. Only the scheme and host should be set.")
|
ctx, proxyURL := mcfg.RequiredString(ctx, "dst-url", "URL to proxy requests to. Only the scheme and host should be set.")
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
||||||
u, err := url.Parse(*proxyURL)
|
u, err := url.Parse(*proxyURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -61,8 +60,8 @@ func main() {
|
|||||||
|
|
||||||
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO mlog.FromHTTP?
|
// TODO mlog.FromHTTP?
|
||||||
authLogger := logger.Clone()
|
// TODO annotate this ctx
|
||||||
authLogger.SetKV(mlog.CtxKV(r.Context()))
|
ctx := r.Context()
|
||||||
|
|
||||||
unauthorized := func() {
|
unauthorized := func() {
|
||||||
w.Header().Add("WWW-Authenticate", "Basic")
|
w.Header().Add("WWW-Authenticate", "Basic")
|
||||||
@ -80,7 +79,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cookie, _ := r.Cookie(*cookieName); cookie != nil {
|
if cookie, _ := r.Cookie(*cookieName); cookie != nil {
|
||||||
authLogger.Debug("authenticating with cookie", mlog.KV{"cookie": cookie.String()})
|
mlog.Debug(ctx, "authenticating with cookie", mlog.KV{"cookie": cookie.String()})
|
||||||
var sig mcrypto.Signature
|
var sig mcrypto.Signature
|
||||||
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
|
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
|
||||||
err := mcrypto.VerifyString(secret, sig, "")
|
err := mcrypto.VerifyString(secret, sig, "")
|
||||||
@ -92,7 +91,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
|
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
|
||||||
logger.Debug("authenticating with user/pass", mlog.KV{
|
mlog.Debug(ctx, "authenticating with user/pass", mlog.KV{
|
||||||
"user": user,
|
"user": user,
|
||||||
"pass": pass,
|
"pass": pass,
|
||||||
})
|
})
|
||||||
@ -107,6 +106,6 @@ func main() {
|
|||||||
unauthorized()
|
unauthorized()
|
||||||
})
|
})
|
||||||
|
|
||||||
mhttp.MListenAndServe(ctx, authHandler)
|
ctx, _ = mhttp.MListenAndServe(ctx, authHandler)
|
||||||
m.Run(ctx)
|
m.Run(ctx)
|
||||||
}
|
}
|
||||||
|
29
m/m.go
29
m/m.go
@ -5,11 +5,11 @@
|
|||||||
package m
|
package m
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
@ -32,20 +32,19 @@ func CfgSource() mcfg.Source {
|
|||||||
// debug information about the running process can be accessed.
|
// debug information about the running process can be accessed.
|
||||||
//
|
//
|
||||||
// TODO set up the debug endpoint.
|
// TODO set up the debug endpoint.
|
||||||
func NewServiceCtx() mctx.Context {
|
func NewServiceCtx() context.Context {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
|
|
||||||
// set up log level handling
|
// set up log level handling
|
||||||
logLevelStr := mcfg.String(ctx, "log-level", "info", "Maximum log level which will be printed.")
|
logger := mlog.NewLogger()
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
ctx = mlog.Set(ctx, logger)
|
||||||
|
ctx, logLevelStr := mcfg.String(ctx, "log-level", "info", "Maximum log level which will be printed.")
|
||||||
|
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
||||||
logLevel := mlog.LevelFromString(*logLevelStr)
|
logLevel := mlog.LevelFromString(*logLevelStr)
|
||||||
if logLevel == nil {
|
if logLevel == nil {
|
||||||
return merr.New("invalid log level", "log-level", *logLevelStr)
|
return merr.New("invalid log level", "log-level", *logLevelStr)
|
||||||
}
|
}
|
||||||
mlog.CtxSetAll(ctx, func(_ mctx.Context, logger *mlog.Logger) *mlog.Logger {
|
logger.SetMaxLevel(logLevel)
|
||||||
logger.SetMaxLevel(logLevel)
|
|
||||||
return logger
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -56,23 +55,23 @@ func NewServiceCtx() mctx.Context {
|
|||||||
// start event, waiting for an interrupt, and then triggering the stop event.
|
// start event, waiting for an interrupt, and then triggering the stop event.
|
||||||
// Run will block until the stop event is done. If any errors are encountered a
|
// Run will block until the stop event is done. If any errors are encountered a
|
||||||
// fatal is thrown.
|
// fatal is thrown.
|
||||||
func Run(ctx mctx.Context) {
|
func Run(ctx context.Context) {
|
||||||
log := mlog.From(ctx)
|
log := mlog.From(ctx)
|
||||||
if err := mcfg.Populate(ctx, CfgSource()); err != nil {
|
if err := mcfg.Populate(ctx, CfgSource()); err != nil {
|
||||||
log.Fatal("error populating configuration", merr.KV(err))
|
log.Fatal(ctx, "error populating configuration", merr.KV(err))
|
||||||
} else if err := mrun.Start(ctx); err != nil {
|
} else if err := mrun.Start(ctx); err != nil {
|
||||||
log.Fatal("error triggering start event", merr.KV(err))
|
log.Fatal(ctx, "error triggering start event", merr.KV(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ch := make(chan os.Signal, 1)
|
ch := make(chan os.Signal, 1)
|
||||||
signal.Notify(ch, os.Interrupt)
|
signal.Notify(ch, os.Interrupt)
|
||||||
s := <-ch
|
s := <-ch
|
||||||
log.Info("signal received, stopping", mlog.KV{"signal": s})
|
log.Info(ctx, "signal received, stopping", mlog.KV{"signal": s})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mrun.Stop(ctx); err != nil {
|
if err := mrun.Stop(ctx); err != nil {
|
||||||
log.Fatal("error triggering stop event", merr.KV(err))
|
log.Fatal(ctx, "error triggering stop event", merr.KV(err))
|
||||||
}
|
}
|
||||||
log.Info("exiting process")
|
log.Info(ctx, "exiting process")
|
||||||
}
|
}
|
||||||
|
13
m/m_test.go
13
m/m_test.go
@ -26,7 +26,8 @@ func TestServiceCtx(t *T) {
|
|||||||
|
|
||||||
// create a child Context before running to ensure it the change propagates
|
// create a child Context before running to ensure it the change propagates
|
||||||
// correctly.
|
// correctly.
|
||||||
ctxA := mctx.ChildOf(ctx, "A")
|
ctxA := mctx.NewChild(ctx, "A")
|
||||||
|
ctx = mctx.WithChild(ctx, ctxA)
|
||||||
|
|
||||||
params := mcfg.ParamValues{{Name: "log-level", Value: json.RawMessage(`"DEBUG"`)}}
|
params := mcfg.ParamValues{{Name: "log-level", Value: json.RawMessage(`"DEBUG"`)}}
|
||||||
if err := mcfg.Populate(ctx, params); err != nil {
|
if err := mcfg.Populate(ctx, params); err != nil {
|
||||||
@ -35,14 +36,16 @@ func TestServiceCtx(t *T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mlog.From(ctxA).Info("foo")
|
mlog.From(ctxA).Info(ctxA, "foo")
|
||||||
mlog.From(ctxA).Debug("bar")
|
mlog.From(ctxA).Debug(ctxA, "bar")
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(msgs, 2),
|
massert.Len(msgs, 2),
|
||||||
massert.Equal(msgs[0].Level.String(), "INFO"),
|
massert.Equal(msgs[0].Level.String(), "INFO"),
|
||||||
massert.Equal(msgs[0].Description.String(), "(/A) foo"),
|
massert.Equal(msgs[0].Description, "foo"),
|
||||||
|
massert.Equal(msgs[0].Context, ctxA),
|
||||||
massert.Equal(msgs[1].Level.String(), "DEBUG"),
|
massert.Equal(msgs[1].Level.String(), "DEBUG"),
|
||||||
massert.Equal(msgs[1].Description.String(), "(/A) bar"),
|
massert.Equal(msgs[1].Description, "bar"),
|
||||||
|
massert.Equal(msgs[1].Context, ctxA),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
|
|
||||||
pvs = append(pvs, ParamValue{
|
pvs = append(pvs, ParamValue{
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Path: p.Path,
|
Path: mctx.Path(p.Context),
|
||||||
Value: p.fuzzyParse(pvStrVal),
|
Value: p.fuzzyParse(pvStrVal),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
func (cli SourceCLI) cliParams(params []Param) (map[string]Param, error) {
|
func (cli SourceCLI) cliParams(params []Param) (map[string]Param, error) {
|
||||||
m := map[string]Param{}
|
m := map[string]Param{}
|
||||||
for _, p := range params {
|
for _, p := range params {
|
||||||
key := strings.Join(append(p.Path, p.Name), cliKeyJoin)
|
key := strings.Join(append(mctx.Path(p.Context), p.Name), cliKeyJoin)
|
||||||
m[cliKeyPrefix+key] = p
|
m[cliKeyPrefix+key] = p
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -2,11 +2,11 @@ package mcfg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -14,12 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSourceCLIHelp(t *T) {
|
func TestSourceCLIHelp(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Int(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
|
ctx, _ = Int(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
|
||||||
Bool(ctx, "bar", "Test bool param.")
|
ctx, _ = Bool(ctx, "bar", "Test bool param.")
|
||||||
String(ctx, "baz", "baz", "Test string param")
|
ctx, _ = String(ctx, "baz", "baz", "Test string param")
|
||||||
RequiredString(ctx, "baz2", "")
|
ctx, _ = RequiredString(ctx, "baz2", "")
|
||||||
RequiredString(ctx, "baz3", "")
|
ctx, _ = RequiredString(ctx, "baz3", "")
|
||||||
src := SourceCLI{}
|
src := SourceCLI{}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
|
|
||||||
pM := map[string]Param{}
|
pM := map[string]Param{}
|
||||||
for _, p := range params {
|
for _, p := range params {
|
||||||
name := env.expectedName(p.Path, p.Name)
|
name := env.expectedName(mctx.Path(p.Context), p.Name)
|
||||||
pM[name] = p
|
pM[name] = p
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
if p, ok := pM[k]; ok {
|
if p, ok := pM[k]; ok {
|
||||||
pvs = append(pvs, ParamValue{
|
pvs = append(pvs, ParamValue{
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Path: p.Path,
|
Path: mctx.Path(p.Context),
|
||||||
Value: p.fuzzyParse(v),
|
Value: p.fuzzyParse(v),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
44
mcfg/mcfg.go
44
mcfg/mcfg.go
@ -3,6 +3,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -17,28 +18,10 @@ import (
|
|||||||
// - JSON file
|
// - JSON file
|
||||||
// - YAML file
|
// - YAML file
|
||||||
|
|
||||||
type ctxCfg struct {
|
|
||||||
path []string
|
|
||||||
params map[string]Param
|
|
||||||
}
|
|
||||||
|
|
||||||
type ctxKey int
|
|
||||||
|
|
||||||
func get(ctx mctx.Context) *ctxCfg {
|
|
||||||
return mctx.GetSetMutableValue(ctx, true, ctxKey(0),
|
|
||||||
func(interface{}) interface{} {
|
|
||||||
return &ctxCfg{
|
|
||||||
path: mctx.Path(ctx),
|
|
||||||
params: map[string]Param{},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).(*ctxCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortParams(params []Param) {
|
func sortParams(params []Param) {
|
||||||
sort.Slice(params, func(i, j int) bool {
|
sort.Slice(params, func(i, j int) bool {
|
||||||
a, b := params[i], params[j]
|
a, b := params[i], params[j]
|
||||||
aPath, bPath := a.Path, b.Path
|
aPath, bPath := mctx.Path(a.Context), mctx.Path(b.Context)
|
||||||
for {
|
for {
|
||||||
switch {
|
switch {
|
||||||
case len(aPath) == 0 && len(bPath) == 0:
|
case len(aPath) == 0 && len(bPath) == 0:
|
||||||
@ -59,12 +42,12 @@ func sortParams(params []Param) {
|
|||||||
// returns all Params gathered by recursively retrieving them from this Context
|
// returns all Params gathered by recursively retrieving them from this Context
|
||||||
// and its children. Returned Params are sorted according to their Path and
|
// and its children. Returned Params are sorted according to their Path and
|
||||||
// Name.
|
// Name.
|
||||||
func collectParams(ctx mctx.Context) []Param {
|
func collectParams(ctx context.Context) []Param {
|
||||||
var params []Param
|
var params []Param
|
||||||
|
|
||||||
var visit func(mctx.Context)
|
var visit func(context.Context)
|
||||||
visit = func(ctx mctx.Context) {
|
visit = func(ctx context.Context) {
|
||||||
for _, param := range get(ctx).params {
|
for _, param := range getLocalParams(ctx) {
|
||||||
params = append(params, param)
|
params = append(params, param)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,9 +81,10 @@ func populate(params []Param, src Source) error {
|
|||||||
// later. There should not be any duplicates here.
|
// later. There should not be any duplicates here.
|
||||||
pM := map[string]Param{}
|
pM := map[string]Param{}
|
||||||
for _, p := range params {
|
for _, p := range params {
|
||||||
hash := paramHash(p.Path, p.Name)
|
path := mctx.Path(p.Context)
|
||||||
|
hash := paramHash(path, p.Name)
|
||||||
if _, ok := pM[hash]; ok {
|
if _, ok := pM[hash]; ok {
|
||||||
panic("duplicate Param: " + paramFullName(p.Path, p.Name))
|
panic("duplicate Param: " + paramFullName(path, p.Name))
|
||||||
}
|
}
|
||||||
pM[hash] = p
|
pM[hash] = p
|
||||||
}
|
}
|
||||||
@ -127,7 +111,7 @@ func populate(params []Param, src Source) error {
|
|||||||
continue
|
continue
|
||||||
} else if _, ok := pvM[hash]; !ok {
|
} else if _, ok := pvM[hash]; !ok {
|
||||||
return merr.New("required parameter is not set",
|
return merr.New("required parameter is not set",
|
||||||
"param", paramFullName(p.Path, p.Name))
|
"param", paramFullName(mctx.Path(p.Context), p.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,12 +128,12 @@ func populate(params []Param, src Source) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate uses the Source to populate the values of all Params which were
|
// Populate uses the Source to populate the values of all Params which were
|
||||||
// added to the given mctx.Context, and all of its children. Populate may be
|
// added to the given Context, and all of its children. Populate may be called
|
||||||
// called multiple times with the same mctx.Context, each time will only affect
|
// multiple times with the same Context, each time will only affect the values
|
||||||
// the values of the Params which were provided by the respective Source.
|
// of the Params which were provided by the respective Source.
|
||||||
//
|
//
|
||||||
// Source may be nil to indicate that no configuration is provided. Only default
|
// Source may be nil to indicate that no configuration is provided. Only default
|
||||||
// values will be used, and if any paramaters are required this will error.
|
// values will be used, and if any paramaters are required this will error.
|
||||||
func Populate(ctx mctx.Context, src Source) error {
|
func Populate(ctx context.Context, src Source) error {
|
||||||
return populate(collectParams(ctx), src)
|
return populate(collectParams(ctx), src)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
@ -9,11 +10,12 @@ import (
|
|||||||
|
|
||||||
func TestPopulate(t *T) {
|
func TestPopulate(t *T) {
|
||||||
{
|
{
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
a := Int(ctx, "a", 0, "")
|
ctx, a := Int(ctx, "a", 0, "")
|
||||||
ctxChild := mctx.ChildOf(ctx, "foo")
|
ctxChild := mctx.NewChild(ctx, "foo")
|
||||||
b := Int(ctxChild, "b", 0, "")
|
ctxChild, b := Int(ctxChild, "b", 0, "")
|
||||||
c := Int(ctxChild, "c", 0, "")
|
ctxChild, c := Int(ctxChild, "c", 0, "")
|
||||||
|
ctx = mctx.WithChild(ctx, ctxChild)
|
||||||
|
|
||||||
err := Populate(ctx, SourceCLI{
|
err := Populate(ctx, SourceCLI{
|
||||||
Args: []string{"--a=1", "--foo-b=2"},
|
Args: []string{"--a=1", "--foo-b=2"},
|
||||||
@ -25,11 +27,12 @@ func TestPopulate(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{ // test that required params are enforced
|
{ // test that required params are enforced
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
a := Int(ctx, "a", 0, "")
|
ctx, a := Int(ctx, "a", 0, "")
|
||||||
ctxChild := mctx.ChildOf(ctx, "foo")
|
ctxChild := mctx.NewChild(ctx, "foo")
|
||||||
b := Int(ctxChild, "b", 0, "")
|
ctxChild, b := Int(ctxChild, "b", 0, "")
|
||||||
c := RequiredInt(ctxChild, "c", "")
|
ctxChild, c := RequiredInt(ctxChild, "c", "")
|
||||||
|
ctx = mctx.WithChild(ctx, ctxChild)
|
||||||
|
|
||||||
err := Populate(ctx, SourceCLI{
|
err := Populate(ctx, SourceCLI{
|
||||||
Args: []string{"--a=1", "--foo-b=2"},
|
Args: []string{"--a=1", "--foo-b=2"},
|
||||||
|
167
mcfg/param.go
167
mcfg/param.go
@ -1,6 +1,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,15 +11,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Param is a configuration parameter which can be populated by Populate. The
|
// Param is a configuration parameter which can be populated by Populate. The
|
||||||
// Param will exist as part of an mctx.Context, relative to its Path. For
|
// Param will exist as part of a Context, relative to its path (see the mctx
|
||||||
// example, a Param with name "addr" under an mctx.Context with Path of
|
// package for more on Context path). For example, a Param with name "addr"
|
||||||
// []string{"foo","bar"} will be setabble on the CLI via "--foo-bar-addr". Other
|
// under a Context with path of []string{"foo","bar"} will be setable on the CLI
|
||||||
// configuration Sources may treat the path/name differently, however.
|
// via "--foo-bar-addr". Other configuration Sources may treat the path/name
|
||||||
|
// differently, however.
|
||||||
//
|
//
|
||||||
// Param values are always unmarshaled as JSON values into the Into field of the
|
// Param values are always unmarshaled as JSON values into the Into field of the
|
||||||
// Param, regardless of the actual Source.
|
// Param, regardless of the actual Source.
|
||||||
type Param struct {
|
type Param struct {
|
||||||
// How the parameter will be identified within an mctx.Context.
|
// How the parameter will be identified within a Context.
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
// A helpful description of how a parameter is expected to be used.
|
// A helpful description of how a parameter is expected to be used.
|
||||||
@ -42,9 +44,9 @@ type Param struct {
|
|||||||
// value of the parameter.
|
// value of the parameter.
|
||||||
Into interface{}
|
Into interface{}
|
||||||
|
|
||||||
// The Path field of the Cfg this Param is attached to. NOTE that this
|
// The Context this Param was added to. NOTE that this will be automatically
|
||||||
// will be automatically filled in when the Param is added to the Cfg.
|
// filled in by MustAdd when the Param is added to the Context.
|
||||||
Path []string
|
Context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func paramFullName(path []string, name string) string {
|
func paramFullName(path []string, name string) string {
|
||||||
@ -65,118 +67,145 @@ func (p Param) fuzzyParse(v string) json.RawMessage {
|
|||||||
return json.RawMessage(v)
|
return json.RawMessage(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustAdd adds the given Param to the mctx.Context. It will panic if a Param of
|
type ctxKey string
|
||||||
// the same Name already exists in the mctx.Context.
|
|
||||||
func MustAdd(ctx mctx.Context, param Param) {
|
func getParam(ctx context.Context, name string) (Param, bool) {
|
||||||
|
param, ok := mctx.LocalValue(ctx, ctxKey(name)).(Param)
|
||||||
|
return param, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustAdd returns a Context with the given Param added to it. It will panic if
|
||||||
|
// a Param with the same Name already exists in the Context.
|
||||||
|
func MustAdd(ctx context.Context, param Param) context.Context {
|
||||||
param.Name = strings.ToLower(param.Name)
|
param.Name = strings.ToLower(param.Name)
|
||||||
param.Path = mctx.Path(ctx)
|
param.Context = ctx
|
||||||
|
|
||||||
cfg := get(ctx)
|
if _, ok := getParam(ctx, param.Name); ok {
|
||||||
if _, ok := cfg.params[param.Name]; ok {
|
path := mctx.Path(ctx)
|
||||||
panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", param.Path, param.Name))
|
panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", path, param.Name))
|
||||||
}
|
}
|
||||||
cfg.params[param.Name] = param
|
|
||||||
|
return mctx.WithLocalValue(ctx, ctxKey(param.Name), param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64 returns an *int64 which will be populated once Populate is run.
|
func getLocalParams(ctx context.Context) []Param {
|
||||||
func Int64(ctx mctx.Context, name string, defaultVal int64, usage string) *int64 {
|
localVals := mctx.LocalValues(ctx)
|
||||||
|
params := make([]Param, 0, len(localVals))
|
||||||
|
for _, val := range localVals {
|
||||||
|
if param, ok := val.(Param); ok {
|
||||||
|
params = append(params, param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 returns an *int64 which will be populated once Populate is run on the
|
||||||
|
// returned Context.
|
||||||
|
func Int64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) {
|
||||||
i := defaultVal
|
i := defaultVal
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
|
||||||
return &i
|
return ctx, &i
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredInt64 returns an *int64 which will be populated once Populate is run,
|
// RequiredInt64 returns an *int64 which will be populated once Populate is run
|
||||||
// and which must be supplied by a configuration Source.
|
// on the returned Context, and which must be supplied by a configuration
|
||||||
func RequiredInt64(ctx mctx.Context, name string, usage string) *int64 {
|
// Source.
|
||||||
|
func RequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) {
|
||||||
var i int64
|
var i int64
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
|
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
|
||||||
return &i
|
return ctx, &i
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int returns an *int which will be populated once Populate is run.
|
// Int returns an *int which will be populated once Populate is run on the
|
||||||
func Int(ctx mctx.Context, name string, defaultVal int, usage string) *int {
|
// returned Context.
|
||||||
|
func Int(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) {
|
||||||
i := defaultVal
|
i := defaultVal
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
|
||||||
return &i
|
return ctx, &i
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredInt returns an *int which will be populated once Populate is run, and
|
// RequiredInt returns an *int which will be populated once Populate is run on
|
||||||
// which must be supplied by a configuration Source.
|
// the returned Context, and which must be supplied by a configuration Source.
|
||||||
func RequiredInt(ctx mctx.Context, name string, usage string) *int {
|
func RequiredInt(ctx context.Context, name string, usage string) (context.Context, *int) {
|
||||||
var i int
|
var i int
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
|
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
|
||||||
return &i
|
return ctx, &i
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a *string which will be populated once Populate is run.
|
// String returns a *string which will be populated once Populate is run on the
|
||||||
func String(ctx mctx.Context, name, defaultVal, usage string) *string {
|
// returned Context.
|
||||||
|
func String(ctx context.Context, name, defaultVal, usage string) (context.Context, *string) {
|
||||||
s := defaultVal
|
s := defaultVal
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s})
|
||||||
return &s
|
return ctx, &s
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredString returns a *string which will be populated once Populate is
|
// RequiredString returns a *string which will be populated once Populate is
|
||||||
// run, and which must be supplied by a configuration Source.
|
// run on the returned Context, and which must be supplied by a configuration
|
||||||
func RequiredString(ctx mctx.Context, name, usage string) *string {
|
// Source.
|
||||||
|
func RequiredString(ctx context.Context, name, usage string) (context.Context, *string) {
|
||||||
var s string
|
var s string
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
|
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
|
||||||
return &s
|
return ctx, &s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bool returns a *bool which will be populated once Populate is run, and which
|
// Bool returns a *bool which will be populated once Populate is run on the
|
||||||
// defaults to false if unconfigured.
|
// returned Context, and which defaults to false if unconfigured.
|
||||||
//
|
//
|
||||||
// The default behavior of all Sources is that a boolean parameter will be set
|
// The default behavior of all Sources is that a boolean parameter will be set
|
||||||
// to true unless the value is "", 0, or false. In the case of the CLI Source
|
// to true unless the value is "", 0, or false. In the case of the CLI Source
|
||||||
// the value will also be true when the parameter is used with no value at all,
|
// the value will also be true when the parameter is used with no value at all,
|
||||||
// as would be expected.
|
// as would be expected.
|
||||||
func Bool(ctx mctx.Context, name, usage string) *bool {
|
func Bool(ctx context.Context, name, usage string) (context.Context, *bool) {
|
||||||
var b bool
|
var b bool
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b})
|
||||||
return &b
|
return ctx, &b
|
||||||
}
|
}
|
||||||
|
|
||||||
// TS returns an *mtime.TS which will be populated once Populate is run.
|
// TS returns an *mtime.TS which will be populated once Populate is run on the
|
||||||
func TS(ctx mctx.Context, name string, defaultVal mtime.TS, usage string) *mtime.TS {
|
// returned Context.
|
||||||
|
func TS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) {
|
||||||
t := defaultVal
|
t := defaultVal
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &t})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &t})
|
||||||
return &t
|
return ctx, &t
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredTS returns an *mtime.TS which will be populated once Populate is run,
|
// RequiredTS returns an *mtime.TS which will be populated once Populate is run
|
||||||
// and which must be supplied by a configuration Source.
|
// on the returned Context, and which must be supplied by a configuration
|
||||||
func RequiredTS(ctx mctx.Context, name, usage string) *mtime.TS {
|
// Source.
|
||||||
|
func RequiredTS(ctx context.Context, name, usage string) (context.Context, *mtime.TS) {
|
||||||
var t mtime.TS
|
var t mtime.TS
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t})
|
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t})
|
||||||
return &t
|
return ctx, &t
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration returns an *mtime.Duration which will be populated once
|
// Duration returns an *mtime.Duration which will be populated once Populate is
|
||||||
// Populate is run.
|
// run on the returned Context.
|
||||||
func Duration(ctx mctx.Context, name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
|
func Duration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
|
||||||
d := defaultVal
|
d := defaultVal
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
|
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
|
||||||
return &d
|
return ctx, &d
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredDuration returns an *mtime.Duration which will be populated once
|
// RequiredDuration returns an *mtime.Duration which will be populated once
|
||||||
// Populate is run, and which must be supplied by a configuration Source.
|
// Populate is run on the returned Context, and which must be supplied by a
|
||||||
func RequiredDuration(ctx mctx.Context, name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
|
// configuration Source.
|
||||||
|
func RequiredDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
|
||||||
var d mtime.Duration
|
var d mtime.Duration
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d})
|
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d})
|
||||||
return &d
|
return ctx, &d
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
||||||
// given interface{} (which should be a pointer). The receiver (into) is also
|
// given interface{} (which should be a pointer). The receiver (into) is also
|
||||||
// used to determine the default value.
|
// used to determine the default value.
|
||||||
func JSON(ctx mctx.Context, name string, into interface{}, usage string) {
|
func JSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
||||||
MustAdd(ctx, Param{Name: name, Usage: usage, Into: into})
|
return MustAdd(ctx, Param{Name: name, Usage: usage, Into: into})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredJSON reads the parameter value as a JSON value and unmarshals it into
|
// RequiredJSON reads the parameter value as a JSON value and unmarshals it into
|
||||||
// the given interface{} (which should be a pointer). The value must be supplied
|
// the given interface{} (which should be a pointer). The value must be supplied
|
||||||
// by a configuration Source.
|
// by a configuration Source.
|
||||||
func RequiredJSON(ctx mctx.Context, name string, into interface{}, usage string) {
|
func RequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
||||||
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
|
return MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
. "testing"
|
. "testing"
|
||||||
@ -16,24 +17,35 @@ import (
|
|||||||
// all the code they share
|
// all the code they share
|
||||||
|
|
||||||
type srcCommonState struct {
|
type srcCommonState struct {
|
||||||
ctx mctx.Context
|
// availCtxs get updated in place as the run goes on, and mkRoot is used to
|
||||||
availCtxs []mctx.Context
|
// create the latest version of the root context based on them
|
||||||
expPVs []ParamValue
|
availCtxs []*context.Context
|
||||||
|
mkRoot func() context.Context
|
||||||
|
|
||||||
|
expPVs []ParamValue
|
||||||
// each specific test should wrap this to add the Source itself
|
// each specific test should wrap this to add the Source itself
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSrcCommonState() srcCommonState {
|
func newSrcCommonState() srcCommonState {
|
||||||
var scs srcCommonState
|
var scs srcCommonState
|
||||||
scs.ctx = mctx.New()
|
|
||||||
{
|
{
|
||||||
a := mctx.ChildOf(scs.ctx, "a")
|
root := context.Background()
|
||||||
b := mctx.ChildOf(scs.ctx, "b")
|
a := mctx.NewChild(root, "a")
|
||||||
c := mctx.ChildOf(scs.ctx, "c")
|
b := mctx.NewChild(root, "b")
|
||||||
ab := mctx.ChildOf(a, "b")
|
c := mctx.NewChild(root, "c")
|
||||||
bc := mctx.ChildOf(b, "c")
|
ab := mctx.NewChild(a, "b")
|
||||||
abc := mctx.ChildOf(ab, "c")
|
bc := mctx.NewChild(b, "c")
|
||||||
scs.availCtxs = []mctx.Context{scs.ctx, a, b, c, ab, bc, abc}
|
abc := mctx.NewChild(ab, "c")
|
||||||
|
scs.availCtxs = []*context.Context{&root, &a, &b, &c, &ab, &bc, &abc}
|
||||||
|
scs.mkRoot = func() context.Context {
|
||||||
|
ab := mctx.WithChild(ab, abc)
|
||||||
|
a := mctx.WithChild(a, ab)
|
||||||
|
b := mctx.WithChild(b, bc)
|
||||||
|
root := mctx.WithChild(root, a)
|
||||||
|
root = mctx.WithChild(root, b)
|
||||||
|
root = mctx.WithChild(root, c)
|
||||||
|
return root
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return scs
|
return scs
|
||||||
}
|
}
|
||||||
@ -57,10 +69,9 @@ func (scs srcCommonState) next() srcCommonParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.availCtxI = mrand.Intn(len(scs.availCtxs))
|
p.availCtxI = mrand.Intn(len(scs.availCtxs))
|
||||||
thisCtx := scs.availCtxs[p.availCtxI]
|
p.path = mctx.Path(*scs.availCtxs[p.availCtxI])
|
||||||
p.path = mctx.Path(thisCtx)
|
|
||||||
|
|
||||||
p.isBool = mrand.Intn(2) == 0
|
p.isBool = mrand.Intn(8) == 0
|
||||||
if !p.isBool {
|
if !p.isBool {
|
||||||
p.nonBoolType = mrand.Element([]string{
|
p.nonBoolType = mrand.Element([]string{
|
||||||
"int",
|
"int",
|
||||||
@ -105,11 +116,11 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
|
|||||||
// the Sources don't actually care about the other fields of Param,
|
// the Sources don't actually care about the other fields of Param,
|
||||||
// those are only used by Populate once it has all ParamValues together
|
// those are only used by Populate once it has all ParamValues together
|
||||||
}
|
}
|
||||||
MustAdd(thisCtx, ctxP)
|
*thisCtx = MustAdd(*thisCtx, ctxP)
|
||||||
ctxP = get(thisCtx).params[p.name] // get it back out to get any added fields
|
ctxP, _ = getParam(*thisCtx, ctxP.Name) // get it back out to get any added fields
|
||||||
|
|
||||||
if !p.unset {
|
if !p.unset {
|
||||||
pv := ParamValue{Name: ctxP.Name, Path: ctxP.Path}
|
pv := ParamValue{Name: ctxP.Name, Path: mctx.Path(ctxP.Context)}
|
||||||
if p.isBool {
|
if p.isBool {
|
||||||
pv.Value = json.RawMessage("true")
|
pv.Value = json.RawMessage("true")
|
||||||
} else {
|
} else {
|
||||||
@ -131,7 +142,8 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
|
|||||||
// given a Source asserts that it's Parse method returns the expected
|
// given a Source asserts that it's Parse method returns the expected
|
||||||
// ParamValues
|
// ParamValues
|
||||||
func (scs srcCommonState) assert(s Source) error {
|
func (scs srcCommonState) assert(s Source) error {
|
||||||
gotPVs, err := s.Parse(collectParams(scs.ctx))
|
root := scs.mkRoot()
|
||||||
|
gotPVs, err := s.Parse(collectParams(root))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -142,10 +154,10 @@ func (scs srcCommonState) assert(s Source) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSources(t *T) {
|
func TestSources(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
a := RequiredInt(ctx, "a", "")
|
ctx, a := RequiredInt(ctx, "a", "")
|
||||||
b := RequiredInt(ctx, "b", "")
|
ctx, b := RequiredInt(ctx, "b", "")
|
||||||
c := RequiredInt(ctx, "c", "")
|
ctx, c := RequiredInt(ctx, "c", "")
|
||||||
|
|
||||||
err := Populate(ctx, Sources{
|
err := Populate(ctx, Sources{
|
||||||
SourceCLI{Args: []string{"--a=1", "--b=666"}},
|
SourceCLI{Args: []string{"--a=1", "--b=666"}},
|
||||||
@ -160,11 +172,12 @@ func TestSources(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSourceParamValues(t *T) {
|
func TestSourceParamValues(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
a := RequiredInt(ctx, "a", "")
|
ctx, a := RequiredInt(ctx, "a", "")
|
||||||
foo := mctx.ChildOf(ctx, "foo")
|
foo := mctx.NewChild(ctx, "foo")
|
||||||
b := RequiredString(foo, "b", "")
|
foo, b := RequiredString(foo, "b", "")
|
||||||
c := Bool(foo, "c", "")
|
foo, c := Bool(foo, "c", "")
|
||||||
|
ctx = mctx.WithChild(ctx, foo)
|
||||||
|
|
||||||
err := Populate(ctx, ParamValues{
|
err := Populate(ctx, ParamValues{
|
||||||
{Name: "a", Value: json.RawMessage(`4`)},
|
{Name: "a", Value: json.RawMessage(`4`)},
|
||||||
|
362
mctx/ctx.go
362
mctx/ctx.go
@ -10,151 +10,163 @@
|
|||||||
package mctx
|
package mctx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"context"
|
||||||
"time"
|
"fmt"
|
||||||
|
|
||||||
goctx "context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is the same as the builtin type, but is used to indicate that the
|
|
||||||
// Context originally came from this package (aka New or ChildOf).
|
|
||||||
type Context goctx.Context
|
|
||||||
|
|
||||||
// CancelFunc is a direct alias of the type from the context package, see its
|
|
||||||
// docs.
|
|
||||||
type CancelFunc = goctx.CancelFunc
|
|
||||||
|
|
||||||
// WithValue mimics the function from the context package.
|
|
||||||
func WithValue(parent Context, key, val interface{}) Context {
|
|
||||||
return Context(goctx.WithValue(goctx.Context(parent), key, val))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCancel mimics the function from the context package.
|
|
||||||
func WithCancel(parent Context) (Context, CancelFunc) {
|
|
||||||
ctx, fn := goctx.WithCancel(goctx.Context(parent))
|
|
||||||
return Context(ctx), fn
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDeadline mimics the function from the context package.
|
|
||||||
func WithDeadline(parent Context, t time.Time) (Context, CancelFunc) {
|
|
||||||
ctx, fn := goctx.WithDeadline(goctx.Context(parent), t)
|
|
||||||
return Context(ctx), fn
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimeout mimics the function from the context package.
|
|
||||||
func WithTimeout(parent Context, d time.Duration) (Context, CancelFunc) {
|
|
||||||
ctx, fn := goctx.WithTimeout(goctx.Context(parent), d)
|
|
||||||
return Context(ctx), fn
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
type mutVal struct {
|
|
||||||
l sync.RWMutex
|
|
||||||
v interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type context struct {
|
|
||||||
goctx.Context
|
|
||||||
|
|
||||||
path []string
|
|
||||||
l sync.RWMutex
|
|
||||||
parent *context
|
|
||||||
children map[string]Context
|
|
||||||
|
|
||||||
mutL sync.RWMutex
|
|
||||||
mutVals map[interface{}]*mutVal
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new context which can be used as the root context for all
|
// New returns a new context which can be used as the root context for all
|
||||||
// purposes in this framework.
|
// purposes in this framework.
|
||||||
func New() Context {
|
//func New() Context {
|
||||||
return &context{Context: goctx.Background()}
|
// return &context{Context: goctx.Background()}
|
||||||
}
|
//}
|
||||||
|
|
||||||
func getCtx(Ctx Context) *context {
|
type ancestryKey int // 0 -> children, 1 -> parent, 2 -> path
|
||||||
ctx, ok := Ctx.(*context)
|
|
||||||
|
const (
|
||||||
|
ancestryKeyChildren ancestryKey = iota
|
||||||
|
ancestryKeyChildrenMap
|
||||||
|
ancestryKeyParent
|
||||||
|
ancestryKeyPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Child returns the Context of the given name which was added to parent via
|
||||||
|
// WithChild, or nil if no Context of that name was ever added.
|
||||||
|
func Child(parent context.Context, name string) context.Context {
|
||||||
|
childrenMap, _ := parent.Value(ancestryKeyChildrenMap).(map[string]int)
|
||||||
|
if len(childrenMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i, ok := childrenMap[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("non-conforming Context used")
|
return nil
|
||||||
}
|
}
|
||||||
return ctx
|
return parent.Value(ancestryKeyChildren).([]context.Context)[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path returns the sequence of names which were used to produce this context
|
// Children returns all children of this Context which have been kept by
|
||||||
// via the ChildOf function.
|
// WithChild, mapped by their name. If this Context wasn't produced by WithChild
|
||||||
func Path(Ctx Context) []string {
|
// then this returns nil.
|
||||||
return getCtx(Ctx).path
|
func Children(parent context.Context) []context.Context {
|
||||||
|
children, _ := parent.Value(ancestryKeyChildren).([]context.Context)
|
||||||
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children returns all children of this context which have been created by
|
func childrenCP(parent context.Context) ([]context.Context, map[string]int) {
|
||||||
// ChildOf, mapped by their name.
|
children := Children(parent)
|
||||||
func Children(Ctx Context) map[string]Context {
|
// plus 1 because this is most commonly used in WithChild, which will append
|
||||||
ctx := getCtx(Ctx)
|
// to it. At any rate it doesn't hurt anything.
|
||||||
out := map[string]Context{}
|
outChildren := make([]context.Context, len(children), len(children)+1)
|
||||||
ctx.l.RLock()
|
copy(outChildren, children)
|
||||||
defer ctx.l.RUnlock()
|
|
||||||
for name, childCtx := range ctx.children {
|
childrenMap, _ := parent.Value(ancestryKeyChildrenMap).(map[string]int)
|
||||||
out[name] = childCtx
|
outChildrenMap := make(map[string]int, len(childrenMap)+1)
|
||||||
|
for name, i := range childrenMap {
|
||||||
|
outChildrenMap[name] = i
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
|
return outChildren, outChildrenMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns the parent Context of the given one, or nil if this is a root
|
// parentOf returns the Context from which this one was generated via NewChild.
|
||||||
// context (i.e. returned from New).
|
// Returns nil if this Context was not generated via NewChild.
|
||||||
func Parent(Ctx Context) Context {
|
|
||||||
return getCtx(Ctx).parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root returns the root Context from which this Context and all of its parents
|
|
||||||
// were derived (i.e. the Context which was originally returned from New).
|
|
||||||
//
|
//
|
||||||
// If the given Context is the root then it is returned as-id.
|
// This is kept private because the behavior is a bit confusing. This will
|
||||||
func Root(Ctx Context) Context {
|
// return the Context which was passed into NewChild, but users would probably
|
||||||
ctx := getCtx(Ctx)
|
// expect it to return the one from WithChild if they were to call this.
|
||||||
for {
|
func parentOf(ctx context.Context) context.Context {
|
||||||
if ctx.parent == nil {
|
parent, _ := ctx.Value(ancestryKeyParent).(context.Context)
|
||||||
return ctx
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the sequence of names which were used to produce this Context
|
||||||
|
// via the NewChild function. If this Context wasn't produced by NewChild then
|
||||||
|
// this returns nil.
|
||||||
|
func Path(ctx context.Context) []string {
|
||||||
|
path, _ := ctx.Value(ancestryKeyPath).([]string)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathCP(ctx context.Context) []string {
|
||||||
|
path := Path(ctx)
|
||||||
|
// plus 1 because this is most commonly used in NewChild, which will append
|
||||||
|
// to it. At any rate it doesn't hurt anything.
|
||||||
|
outPath := make([]string, len(path), len(path)+1)
|
||||||
|
copy(outPath, path)
|
||||||
|
return outPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name this Context was generated with via NewChild, or false
|
||||||
|
// if this Context was not generated via NewChild.
|
||||||
|
func Name(ctx context.Context) (string, bool) {
|
||||||
|
path := Path(ctx)
|
||||||
|
if len(path) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return path[len(path)-1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChild creates a new Context based off of the parent one, and returns a new
|
||||||
|
// instance of the passed in parent and the new child. The child will have a
|
||||||
|
// path which is the parent's path with the given name appended. The parent will
|
||||||
|
// have the new child as part of its set of children (see Children function).
|
||||||
|
//
|
||||||
|
// If the parent already has a child of the given name this function panics.
|
||||||
|
func NewChild(parent context.Context, name string) context.Context {
|
||||||
|
if Child(parent, name) != nil {
|
||||||
|
panic(fmt.Sprintf("child with name %q already exists on parent", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
childPath := append(pathCP(parent), name)
|
||||||
|
child := withoutLocalValues(parent)
|
||||||
|
child = context.WithValue(child, ancestryKeyChildren, nil) // unset children
|
||||||
|
child = context.WithValue(child, ancestryKeyChildrenMap, nil) // unset children
|
||||||
|
child = context.WithValue(child, ancestryKeyParent, parent)
|
||||||
|
child = context.WithValue(child, ancestryKeyPath, childPath)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
func isChild(parent, child context.Context) bool {
|
||||||
|
parentPath, childPath := Path(parent), Path(child)
|
||||||
|
if len(parentPath) != len(childPath)-1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range parentPath {
|
||||||
|
if parentPath[i] != childPath[i] {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
ctx = ctx.parent
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChildOf creates a child of the given context with the given name and returns
|
// WithChild returns a modified parent which holds a reference to child in its
|
||||||
// it. The Path of the returned context will be the path of the parent with its
|
// Children set. If the child's name is already taken in the parent then this
|
||||||
// name appended to it. The Children function can be called on the parent to
|
// function panics.
|
||||||
// retrieve all children which have been made using this function.
|
func WithChild(parent, child context.Context) context.Context {
|
||||||
//
|
if !isChild(parent, child) {
|
||||||
// TODO If the given Context already has a child with the given name that child
|
panic(fmt.Sprintf("child cannot be kept by Context which is not its parent"))
|
||||||
// will be returned.
|
|
||||||
func ChildOf(Ctx Context, name string) Context {
|
|
||||||
ctx, childCtx := getCtx(Ctx), new(context)
|
|
||||||
|
|
||||||
ctx.l.Lock()
|
|
||||||
defer ctx.l.Unlock()
|
|
||||||
|
|
||||||
// set child's path field
|
|
||||||
childCtx.path = make([]string, 0, len(ctx.path)+1)
|
|
||||||
childCtx.path = append(childCtx.path, ctx.path...)
|
|
||||||
childCtx.path = append(childCtx.path, name)
|
|
||||||
|
|
||||||
// set child's parent field
|
|
||||||
childCtx.parent = ctx
|
|
||||||
|
|
||||||
// create child's ctx and store it in parent
|
|
||||||
if ctx.children == nil {
|
|
||||||
ctx.children = map[string]Context{}
|
|
||||||
}
|
}
|
||||||
ctx.children[name] = childCtx
|
|
||||||
return childCtx
|
name, _ := Name(child)
|
||||||
|
children, childrenMap := childrenCP(parent)
|
||||||
|
if _, ok := childrenMap[name]; ok {
|
||||||
|
panic(fmt.Sprintf("child with name %q already exists on parent", name))
|
||||||
|
}
|
||||||
|
children = append(children, child)
|
||||||
|
childrenMap[name] = len(children) - 1
|
||||||
|
|
||||||
|
parent = context.WithValue(parent, ancestryKeyChildren, children)
|
||||||
|
parent = context.WithValue(parent, ancestryKeyChildrenMap, childrenMap)
|
||||||
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreadthFirstVisit visits this Context and all of its children, and their
|
// BreadthFirstVisit visits this Context and all of its children, and their
|
||||||
// children, in a breadth-first order. If the callback returns false then the
|
// children, in a breadth-first order. If the callback returns false then the
|
||||||
// function returns without visiting any more Contexts.
|
// function returns without visiting any more Contexts.
|
||||||
//
|
func BreadthFirstVisit(ctx context.Context, callback func(context.Context) bool) {
|
||||||
// The exact order of visitation is non-deterministic.
|
queue := []context.Context{ctx}
|
||||||
func BreadthFirstVisit(Ctx Context, callback func(Context) bool) {
|
|
||||||
queue := []Context{Ctx}
|
|
||||||
for len(queue) > 0 {
|
for len(queue) > 0 {
|
||||||
if !callback(queue[0]) {
|
if !callback(queue[0]) {
|
||||||
return
|
return
|
||||||
@ -167,78 +179,56 @@ func BreadthFirstVisit(Ctx Context, callback func(Context) bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// code related to mutable values
|
// local value stuff
|
||||||
|
|
||||||
// MutableValue acts like the Value method, except that it only deals with
|
type localValsKey int
|
||||||
// keys/values set by SetMutableValue.
|
|
||||||
func MutableValue(Ctx Context, key interface{}) interface{} {
|
type localVal struct {
|
||||||
ctx := getCtx(Ctx)
|
prev *localVal
|
||||||
ctx.mutL.RLock()
|
key, val interface{}
|
||||||
defer ctx.mutL.RUnlock()
|
|
||||||
if ctx.mutVals == nil {
|
|
||||||
return nil
|
|
||||||
} else if mVal, ok := ctx.mutVals[key]; ok {
|
|
||||||
mVal.l.RLock()
|
|
||||||
defer mVal.l.RUnlock()
|
|
||||||
return mVal.v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSetMutableValue is used to interact with a mutable value on the context in
|
// WithLocalValue is like WithValue, but the stored value will not be present
|
||||||
// a thread-safe way. The key's value is retrieved and passed to the callback.
|
// on any children created via WithChild. Local values must be retrieved with
|
||||||
// The value returned from the callback is stored back into the context as well
|
// the LocalValue function in this package. Local values share a different
|
||||||
// as being returned from this function.
|
// namespace than the normal WithValue/Value values (i.e. they do not overlap).
|
||||||
//
|
func WithLocalValue(ctx context.Context, key, val interface{}) context.Context {
|
||||||
// If noCallbackIfSet is set to true, then if the key is already set the value
|
prev, _ := ctx.Value(localValsKey(0)).(*localVal)
|
||||||
// will be returned without calling the callback.
|
return context.WithValue(ctx, localValsKey(0), &localVal{
|
||||||
//
|
prev: prev,
|
||||||
// The callback returning nil is equivalent to unsetting the value.
|
key: key, val: val,
|
||||||
//
|
})
|
||||||
// Children of this context will _not_ inherit any of its mutable values.
|
}
|
||||||
//
|
|
||||||
// Within the callback it is fine to call other functions/methods on the
|
|
||||||
// Context, except for those related to mutable values for this same key (e.g.
|
|
||||||
// MutableValue and SetMutableValue).
|
|
||||||
func GetSetMutableValue(
|
|
||||||
Ctx Context, noCallbackIfSet bool,
|
|
||||||
key interface{}, fn func(interface{}) interface{},
|
|
||||||
) interface{} {
|
|
||||||
|
|
||||||
// if noCallbackIfSet, do a fast lookup with MutableValue first.
|
func withoutLocalValues(ctx context.Context) context.Context {
|
||||||
if noCallbackIfSet {
|
return context.WithValue(ctx, localValsKey(0), nil)
|
||||||
if v := MutableValue(Ctx, key); v != nil {
|
}
|
||||||
return v
|
|
||||||
|
// LocalValue returns the value for the given key which was set by a call to
|
||||||
|
// WithLocalValue, or nil if no value was set for the given key.
|
||||||
|
func LocalValue(ctx context.Context, key interface{}) interface{} {
|
||||||
|
lv, _ := ctx.Value(localValsKey(0)).(*localVal)
|
||||||
|
for {
|
||||||
|
if lv == nil {
|
||||||
|
return nil
|
||||||
|
} else if lv.key == key {
|
||||||
|
return lv.val
|
||||||
}
|
}
|
||||||
|
lv = lv.prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalValues returns all key/value pairs which have been set on the Context
|
||||||
|
// via WithLocalValue.
|
||||||
|
func LocalValues(ctx context.Context) map[interface{}]interface{} {
|
||||||
|
m := map[interface{}]interface{}{}
|
||||||
|
lv, _ := ctx.Value(localValsKey(0)).(*localVal)
|
||||||
|
for {
|
||||||
|
if lv == nil {
|
||||||
|
return m
|
||||||
|
} else if _, ok := m[lv.key]; !ok {
|
||||||
|
m[lv.key] = lv.val
|
||||||
|
}
|
||||||
|
lv = lv.prev
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := getCtx(Ctx)
|
|
||||||
ctx.mutL.Lock()
|
|
||||||
if ctx.mutVals == nil {
|
|
||||||
ctx.mutVals = map[interface{}]*mutVal{}
|
|
||||||
}
|
|
||||||
mVal, ok := ctx.mutVals[key]
|
|
||||||
if !ok {
|
|
||||||
mVal = new(mutVal)
|
|
||||||
ctx.mutVals[key] = mVal
|
|
||||||
}
|
|
||||||
ctx.mutL.Unlock()
|
|
||||||
|
|
||||||
mVal.l.Lock()
|
|
||||||
defer mVal.l.Unlock()
|
|
||||||
|
|
||||||
// It's possible something happened between the first check inside the
|
|
||||||
// read-lock and now, so double check this case. It's still good to have the
|
|
||||||
// read-lock check there, it'll handle 99% of the cases.
|
|
||||||
if noCallbackIfSet && mVal.v != nil {
|
|
||||||
return mVal.v
|
|
||||||
}
|
|
||||||
|
|
||||||
mVal.v = fn(mVal.v)
|
|
||||||
|
|
||||||
// TODO if the new v is nil then key could be deleted out of mutVals. But
|
|
||||||
// doing so would be weird in the case that there's another routine which
|
|
||||||
// has already pulled this same mVal out of mutVals and is waiting on its
|
|
||||||
// mutex.
|
|
||||||
return mVal.v
|
|
||||||
}
|
}
|
||||||
|
215
mctx/ctx_test.go
215
mctx/ctx_test.go
@ -1,18 +1,22 @@
|
|||||||
package mctx
|
package mctx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"context"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInheritance(t *T) {
|
func TestInheritance(t *T) {
|
||||||
ctx := New()
|
ctx := context.Background()
|
||||||
ctx1 := ChildOf(ctx, "1")
|
ctx1 := NewChild(ctx, "1")
|
||||||
ctx1a := ChildOf(ctx1, "a")
|
ctx1a := NewChild(ctx1, "a")
|
||||||
ctx1b := ChildOf(ctx1, "b")
|
ctx1b := NewChild(ctx1, "b")
|
||||||
ctx2 := ChildOf(ctx, "2")
|
ctx1 = WithChild(ctx1, ctx1a)
|
||||||
|
ctx1 = WithChild(ctx1, ctx1b)
|
||||||
|
ctx2 := NewChild(ctx, "2")
|
||||||
|
ctx = WithChild(ctx, ctx1)
|
||||||
|
ctx = WithChild(ctx, ctx2)
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(Path(ctx), 0),
|
massert.Len(Path(ctx), 0),
|
||||||
@ -23,129 +27,130 @@ func TestInheritance(t *T) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Equal(
|
massert.Equal([]context.Context{ctx1, ctx2}, Children(ctx)),
|
||||||
map[string]Context{"1": ctx1, "2": ctx2},
|
massert.Equal([]context.Context{ctx1a, ctx1b}, Children(ctx1)),
|
||||||
Children(ctx),
|
massert.Len(Children(ctx2), 0),
|
||||||
),
|
|
||||||
massert.Equal(
|
|
||||||
map[string]Context{"a": ctx1a, "b": ctx1b},
|
|
||||||
Children(ctx1),
|
|
||||||
),
|
|
||||||
massert.Equal(
|
|
||||||
map[string]Context{},
|
|
||||||
Children(ctx2),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
|
||||||
massert.Nil(Parent(ctx)),
|
|
||||||
massert.Equal(Parent(ctx1), ctx),
|
|
||||||
massert.Equal(Parent(ctx1a), ctx1),
|
|
||||||
massert.Equal(Parent(ctx1b), ctx1),
|
|
||||||
massert.Equal(Parent(ctx2), ctx),
|
|
||||||
))
|
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
|
||||||
massert.Equal(Root(ctx), ctx),
|
|
||||||
massert.Equal(Root(ctx1), ctx),
|
|
||||||
massert.Equal(Root(ctx1a), ctx),
|
|
||||||
massert.Equal(Root(ctx1b), ctx),
|
|
||||||
massert.Equal(Root(ctx2), ctx),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBreadFirstVisit(t *T) {
|
func TestBreadFirstVisit(t *T) {
|
||||||
ctx := New()
|
ctx := context.Background()
|
||||||
ctx1 := ChildOf(ctx, "1")
|
ctx1 := NewChild(ctx, "1")
|
||||||
ctx1a := ChildOf(ctx1, "a")
|
ctx1a := NewChild(ctx1, "a")
|
||||||
ctx1b := ChildOf(ctx1, "b")
|
ctx1b := NewChild(ctx1, "b")
|
||||||
ctx2 := ChildOf(ctx, "2")
|
ctx1 = WithChild(ctx1, ctx1a)
|
||||||
|
ctx1 = WithChild(ctx1, ctx1b)
|
||||||
|
ctx2 := NewChild(ctx, "2")
|
||||||
|
ctx = WithChild(ctx, ctx1)
|
||||||
|
ctx = WithChild(ctx, ctx2)
|
||||||
|
|
||||||
{
|
{
|
||||||
got := make([]Context, 0, 5)
|
got := make([]context.Context, 0, 5)
|
||||||
BreadthFirstVisit(ctx, func(ctx Context) bool {
|
BreadthFirstVisit(ctx, func(ctx context.Context) bool {
|
||||||
got = append(got, ctx)
|
got = append(got, ctx)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
// since children are stored in a map the exact order is non-deterministic
|
massert.Fatal(t,
|
||||||
massert.Fatal(t, massert.Any(
|
massert.Equal([]context.Context{ctx, ctx1, ctx2, ctx1a, ctx1b}, got),
|
||||||
massert.Equal([]Context{ctx, ctx1, ctx2, ctx1a, ctx1b}, got),
|
)
|
||||||
massert.Equal([]Context{ctx, ctx1, ctx2, ctx1b, ctx1a}, got),
|
|
||||||
massert.Equal([]Context{ctx, ctx2, ctx1, ctx1a, ctx1b}, got),
|
|
||||||
massert.Equal([]Context{ctx, ctx2, ctx1, ctx1b, ctx1a}, got),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
got := make([]Context, 0, 3)
|
got := make([]context.Context, 0, 3)
|
||||||
BreadthFirstVisit(ctx, func(ctx Context) bool {
|
BreadthFirstVisit(ctx, func(ctx context.Context) bool {
|
||||||
if len(Path(ctx)) > 1 {
|
if len(Path(ctx)) > 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
got = append(got, ctx)
|
got = append(got, ctx)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
massert.Fatal(t, massert.Any(
|
massert.Fatal(t,
|
||||||
massert.Equal([]Context{ctx, ctx1, ctx2}, got),
|
massert.Equal([]context.Context{ctx, ctx1, ctx2}, got),
|
||||||
massert.Equal([]Context{ctx, ctx2, ctx1}, got),
|
)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMutableValues(t *T) {
|
func TestLocalValues(t *T) {
|
||||||
fn := func(v interface{}) interface{} {
|
|
||||||
if v == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return v.(int) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var aa []massert.Assertion
|
// test with no value set
|
||||||
|
ctx := context.Background()
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Nil(LocalValue(ctx, "foo")),
|
||||||
|
massert.Len(LocalValues(ctx), 0),
|
||||||
|
))
|
||||||
|
|
||||||
ctx := New()
|
// test basic value retrieval
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx, false, 0, fn), 0))
|
ctx = WithLocalValue(ctx, "foo", "bar")
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx, false, 0, fn), 1))
|
massert.Fatal(t, massert.All(
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx, true, 0, fn), 1))
|
massert.Equal("bar", LocalValue(ctx, "foo")),
|
||||||
|
massert.Equal(
|
||||||
|
map[interface{}]interface{}{"foo": "bar"},
|
||||||
|
LocalValues(ctx),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
aa = append(aa, massert.Equal(MutableValue(ctx, 0), 1))
|
// test that doesn't conflict with WithValue
|
||||||
|
ctx = context.WithValue(ctx, "foo", "WithValue bar")
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Equal("bar", LocalValue(ctx, "foo")),
|
||||||
|
massert.Equal("WithValue bar", ctx.Value("foo")),
|
||||||
|
massert.Equal(
|
||||||
|
map[interface{}]interface{}{"foo": "bar"},
|
||||||
|
LocalValues(ctx),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
ctx1 := ChildOf(ctx, "one")
|
// test that child doesn't get values
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx1, true, 0, fn), 0))
|
child := NewChild(ctx, "child")
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx1, false, 0, fn), 1))
|
massert.Fatal(t, massert.All(
|
||||||
aa = append(aa, massert.Equal(GetSetMutableValue(ctx1, true, 0, fn), 1))
|
massert.Equal("bar", LocalValue(ctx, "foo")),
|
||||||
|
massert.Nil(LocalValue(child, "foo")),
|
||||||
|
massert.Len(LocalValues(child), 0),
|
||||||
|
))
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(aa...))
|
// test that values on child don't affect parent values
|
||||||
}
|
child = WithLocalValue(child, "foo", "child bar")
|
||||||
|
ctx = WithChild(ctx, child)
|
||||||
func TestMutableValuesParallel(t *T) {
|
massert.Fatal(t, massert.All(
|
||||||
const events = 1000000
|
massert.Equal("bar", LocalValue(ctx, "foo")),
|
||||||
const workers = 10
|
massert.Equal("child bar", LocalValue(child, "foo")),
|
||||||
|
massert.Equal(
|
||||||
incr := func(v interface{}) interface{} {
|
map[interface{}]interface{}{"foo": "bar"},
|
||||||
if v == nil {
|
LocalValues(ctx),
|
||||||
return 1
|
),
|
||||||
}
|
massert.Equal(
|
||||||
return v.(int) + 1
|
map[interface{}]interface{}{"foo": "child bar"},
|
||||||
}
|
LocalValues(child),
|
||||||
|
),
|
||||||
ch := make(chan bool, events)
|
))
|
||||||
for i := 0; i < events; i++ {
|
|
||||||
ch <- true
|
// test that two With calls on the same context generate distinct contexts
|
||||||
}
|
childA := WithLocalValue(child, "foo2", "baz")
|
||||||
close(ch)
|
childB := WithLocalValue(child, "foo2", "buz")
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
ctx := New()
|
massert.Equal("bar", LocalValue(ctx, "foo")),
|
||||||
wg := new(sync.WaitGroup)
|
massert.Equal("child bar", LocalValue(child, "foo")),
|
||||||
for i := 0; i < workers; i++ {
|
massert.Nil(LocalValue(child, "foo2")),
|
||||||
wg.Add(1)
|
massert.Equal("baz", LocalValue(childA, "foo2")),
|
||||||
go func() {
|
massert.Equal("buz", LocalValue(childB, "foo2")),
|
||||||
defer wg.Done()
|
massert.Equal(
|
||||||
for range ch {
|
map[interface{}]interface{}{"foo": "child bar", "foo2": "baz"},
|
||||||
GetSetMutableValue(ctx, false, 0, incr)
|
LocalValues(childA),
|
||||||
}
|
),
|
||||||
}()
|
massert.Equal(
|
||||||
}
|
map[interface{}]interface{}{"foo": "child bar", "foo2": "buz"},
|
||||||
|
LocalValues(childB),
|
||||||
wg.Wait()
|
),
|
||||||
massert.Fatal(t, massert.Equal(events, MutableValue(ctx, 0)))
|
))
|
||||||
|
|
||||||
|
// if a value overwrites a previous one the newer one should show in
|
||||||
|
// LocalValues
|
||||||
|
ctx = WithLocalValue(ctx, "foo", "barbar")
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Equal("barbar", LocalValue(ctx, "foo")),
|
||||||
|
massert.Equal(
|
||||||
|
map[interface{}]interface{}{"foo": "barbar"},
|
||||||
|
LocalValues(ctx),
|
||||||
|
),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func isErrAlreadyExists(err error) bool {
|
|||||||
type BigQuery struct {
|
type BigQuery struct {
|
||||||
*bigquery.Client
|
*bigquery.Client
|
||||||
gce *mdb.GCE
|
gce *mdb.GCE
|
||||||
log *mlog.Logger
|
ctx context.Context
|
||||||
|
|
||||||
// key is dataset/tableName
|
// key is dataset/tableName
|
||||||
tablesL sync.Mutex
|
tablesL sync.Mutex
|
||||||
@ -43,35 +43,37 @@ type BigQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a BigQuery instance which will be initialized and configured
|
// MNew returns a BigQuery instance which will be initialized and configured
|
||||||
// when the start event is triggered on ctx (see mrun.Start). The BigQuery
|
// when the start event is triggered on the returned (see mrun.Start). The
|
||||||
// instance will have Close called on it when the stop event is triggered on ctx
|
// BigQuery instance will have Close called on it when the stop event is
|
||||||
// (see mrun.Stop).
|
// triggered on the returned Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// gce is optional and can be passed in if there's an existing gce object which
|
// gce is optional and can be passed in if there's an existing gce object which
|
||||||
// should be used, otherwise a new one will be created with mdb.MGCE.
|
// should be used, otherwise a new one will be created with mdb.MGCE.
|
||||||
func MNew(ctx mctx.Context, gce *mdb.GCE) *BigQuery {
|
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *BigQuery) {
|
||||||
if gce == nil {
|
if gce == nil {
|
||||||
gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.MGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = mctx.ChildOf(ctx, "bigquery")
|
|
||||||
bq := &BigQuery{
|
bq := &BigQuery{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
tables: map[[2]string]*bigquery.Table{},
|
tables: map[[2]string]*bigquery.Table{},
|
||||||
tableUploaders: map[[2]string]*bigquery.Uploader{},
|
tableUploaders: map[[2]string]*bigquery.Uploader{},
|
||||||
log: mlog.From(ctx),
|
ctx: mctx.NewChild(ctx, "bigquery"),
|
||||||
}
|
}
|
||||||
bq.log.SetKV(bq)
|
|
||||||
mrun.OnStart(ctx, func(innerCtx mctx.Context) error {
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
bq.log.Info("connecting to bigquery")
|
// bq.log.SetKV(bq)
|
||||||
|
|
||||||
|
bq.ctx = mrun.OnStart(bq.ctx, func(innerCtx context.Context) error {
|
||||||
|
mlog.Info(bq.ctx, "connecting to bigquery")
|
||||||
var err error
|
var err error
|
||||||
bq.Client, err = bigquery.NewClient(innerCtx, bq.gce.Project, bq.gce.ClientOptions()...)
|
bq.Client, err = bigquery.NewClient(innerCtx, bq.gce.Project, bq.gce.ClientOptions()...)
|
||||||
return merr.WithKV(err, bq.KV())
|
return merr.WithKV(err, bq.KV())
|
||||||
})
|
})
|
||||||
mrun.OnStop(ctx, func(mctx.Context) error {
|
bq.ctx = mrun.OnStop(bq.ctx, func(context.Context) error {
|
||||||
return bq.Client.Close()
|
return bq.Client.Close()
|
||||||
})
|
})
|
||||||
return bq
|
return mctx.WithChild(ctx, bq.ctx), bq
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
// KV implements the mlog.KVer interface.
|
||||||
@ -99,7 +101,7 @@ func (bq *BigQuery) Table(
|
|||||||
}
|
}
|
||||||
|
|
||||||
kv := mlog.KV{"bigQueryDataset": dataset, "bigQueryTable": tableName}
|
kv := mlog.KV{"bigQueryDataset": dataset, "bigQueryTable": tableName}
|
||||||
bq.log.Debug("creating/grabbing table", kv)
|
mlog.Debug(bq.ctx, "creating/grabbing table", kv)
|
||||||
|
|
||||||
schema, err := bigquery.InferSchema(schemaObj)
|
schema, err := bigquery.InferSchema(schemaObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -28,44 +28,45 @@ type Bigtable struct {
|
|||||||
Instance string
|
Instance string
|
||||||
|
|
||||||
gce *mdb.GCE
|
gce *mdb.GCE
|
||||||
log *mlog.Logger
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a Bigtable instance which will be initialized and configured
|
// MNew returns a Bigtable instance which will be initialized and configured
|
||||||
// when the start event is triggered on ctx (see mrun.Start). The Bigtable
|
// when the start event is triggered on the returned Context (see mrun.Start).
|
||||||
// instance will have Close called on it when the stop event is triggered on ctx
|
// The Bigtable instance will have Close called on it when the stop event is
|
||||||
// (see mrun.Stop).
|
// triggered on the returned Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// gce is optional and can be passed in if there's an existing gce object which
|
// gce is optional and can be passed in if there's an existing gce object which
|
||||||
// should be used, otherwise a new one will be created with mdb.MGCE.
|
// should be used, otherwise a new one will be created with mdb.MGCE.
|
||||||
//
|
//
|
||||||
// defaultInstance can be given as the instance name to use as the default
|
// defaultInstance can be given as the instance name to use as the default
|
||||||
// parameter value. If empty the parameter will be required to be set.
|
// parameter value. If empty the parameter will be required to be set.
|
||||||
func MNew(ctx mctx.Context, gce *mdb.GCE, defaultInstance string) *Bigtable {
|
func MNew(ctx context.Context, gce *mdb.GCE, defaultInstance string) (context.Context, *Bigtable) {
|
||||||
if gce == nil {
|
if gce == nil {
|
||||||
gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.MGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = mctx.ChildOf(ctx, "bigtable")
|
|
||||||
bt := &Bigtable{
|
bt := &Bigtable{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
log: mlog.From(ctx),
|
ctx: mctx.NewChild(ctx, "bigtable"),
|
||||||
}
|
}
|
||||||
bt.log.SetKV(bt)
|
|
||||||
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
|
// bt.log.SetKV(bt)
|
||||||
|
|
||||||
var inst *string
|
var inst *string
|
||||||
{
|
{
|
||||||
const name, descr = "instance", "name of the bigtable instance in the project to connect to"
|
const name, descr = "instance", "name of the bigtable instance in the project to connect to"
|
||||||
if defaultInstance != "" {
|
if defaultInstance != "" {
|
||||||
inst = mcfg.String(ctx, name, defaultInstance, descr)
|
bt.ctx, inst = mcfg.String(bt.ctx, name, defaultInstance, descr)
|
||||||
} else {
|
} else {
|
||||||
inst = mcfg.RequiredString(ctx, name, descr)
|
bt.ctx, inst = mcfg.RequiredString(bt.ctx, name, descr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mrun.OnStart(ctx, func(innerCtx mctx.Context) error {
|
bt.ctx = mrun.OnStart(bt.ctx, func(innerCtx context.Context) error {
|
||||||
bt.Instance = *inst
|
bt.Instance = *inst
|
||||||
bt.log.Info("connecting to bigtable")
|
mlog.Info(bt.ctx, "connecting to bigtable", bt)
|
||||||
var err error
|
var err error
|
||||||
bt.Client, err = bigtable.NewClient(
|
bt.Client, err = bigtable.NewClient(
|
||||||
innerCtx,
|
innerCtx,
|
||||||
@ -74,10 +75,10 @@ func MNew(ctx mctx.Context, gce *mdb.GCE, defaultInstance string) *Bigtable {
|
|||||||
)
|
)
|
||||||
return merr.WithKV(err, bt.KV())
|
return merr.WithKV(err, bt.KV())
|
||||||
})
|
})
|
||||||
mrun.OnStop(ctx, func(mctx.Context) error {
|
bt.ctx = mrun.OnStop(bt.ctx, func(context.Context) error {
|
||||||
return bt.Client.Close()
|
return bt.Client.Close()
|
||||||
})
|
})
|
||||||
return bt
|
return mctx.WithChild(ctx, bt.ctx), bt
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
// KV implements the mlog.KVer interface.
|
||||||
@ -93,16 +94,16 @@ func (bt *Bigtable) KV() map[string]interface{} {
|
|||||||
// This method requires admin privileges on the bigtable instance.
|
// This method requires admin privileges on the bigtable instance.
|
||||||
func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...string) error {
|
func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...string) error {
|
||||||
kv := mlog.KV{"bigtableTable": name}
|
kv := mlog.KV{"bigtableTable": name}
|
||||||
bt.log.Info("ensuring table", kv)
|
mlog.Info(bt.ctx, "ensuring table", kv)
|
||||||
|
|
||||||
bt.log.Debug("creating admin client", kv)
|
mlog.Debug(bt.ctx, "creating admin client", kv)
|
||||||
adminClient, err := bigtable.NewAdminClient(ctx, bt.gce.Project, bt.Instance)
|
adminClient, err := bigtable.NewAdminClient(ctx, bt.gce.Project, bt.Instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.WithKV(err, bt.KV(), kv.KV())
|
||||||
}
|
}
|
||||||
defer adminClient.Close()
|
defer adminClient.Close()
|
||||||
|
|
||||||
bt.log.Debug("creating bigtable table (if needed)", kv)
|
mlog.Debug(bt.ctx, "creating bigtable table (if needed)", kv)
|
||||||
err = adminClient.CreateTable(ctx, name)
|
err = adminClient.CreateTable(ctx, name)
|
||||||
if err != nil && !isErrAlreadyExists(err) {
|
if err != nil && !isErrAlreadyExists(err) {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.WithKV(err, bt.KV(), kv.KV())
|
||||||
@ -110,7 +111,7 @@ func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...str
|
|||||||
|
|
||||||
for _, colFam := range colFams {
|
for _, colFam := range colFams {
|
||||||
kv := kv.Set("family", colFam)
|
kv := kv.Set("family", colFam)
|
||||||
bt.log.Debug("creating bigtable column family (if needed)", kv)
|
mlog.Debug(bt.ctx, "creating bigtable column family (if needed)", kv)
|
||||||
err := adminClient.CreateColumnFamily(ctx, name, colFam)
|
err := adminClient.CreateColumnFamily(ctx, name, colFam)
|
||||||
if err != nil && !isErrAlreadyExists(err) {
|
if err != nil && !isErrAlreadyExists(err) {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.WithKV(err, bt.KV(), kv.KV())
|
||||||
|
@ -12,8 +12,8 @@ import (
|
|||||||
|
|
||||||
func TestBasic(t *T) {
|
func TestBasic(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
mtest.SetEnv(ctx, "GCE_PROJECT", "testProject")
|
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "testProject")
|
||||||
bt := MNew(ctx, nil, "testInstance")
|
ctx, bt := MNew(ctx, nil, "testInstance")
|
||||||
|
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
tableName := "test-" + mrand.Hex(8)
|
tableName := "test-" + mrand.Hex(8)
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
package mdatastore
|
package mdatastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"cloud.google.com/go/datastore"
|
"cloud.google.com/go/datastore"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mdb"
|
"github.com/mediocregopher/mediocre-go-lib/mdb"
|
||||||
@ -17,38 +19,39 @@ type Datastore struct {
|
|||||||
*datastore.Client
|
*datastore.Client
|
||||||
|
|
||||||
gce *mdb.GCE
|
gce *mdb.GCE
|
||||||
log *mlog.Logger
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a Datastore instance which will be initialized and configured
|
// MNew returns a Datastore instance which will be initialized and configured
|
||||||
// when the start event is triggered on ctx (see mrun.Start). The Datastore
|
// when the start event is triggered on the returned Context (see mrun.Start).
|
||||||
// instance will have Close called on it when the stop event is triggered on ctx
|
// The Datastore instance will have Close called on it when the stop event is
|
||||||
// (see mrun.Stop).
|
// triggered on the returned Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// gce is optional and can be passed in if there's an existing gce object which
|
// gce is optional and can be passed in if there's an existing gce object which
|
||||||
// should be used, otherwise a new one will be created with mdb.MGCE.
|
// should be used, otherwise a new one will be created with mdb.MGCE.
|
||||||
func MNew(ctx mctx.Context, gce *mdb.GCE) *Datastore {
|
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *Datastore) {
|
||||||
if gce == nil {
|
if gce == nil {
|
||||||
gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.MGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = mctx.ChildOf(ctx, "datastore")
|
|
||||||
ds := &Datastore{
|
ds := &Datastore{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
log: mlog.From(ctx),
|
ctx: mctx.NewChild(ctx, "datastore"),
|
||||||
}
|
}
|
||||||
ds.log.SetKV(ds)
|
|
||||||
|
|
||||||
mrun.OnStart(ctx, func(innerCtx mctx.Context) error {
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
ds.log.Info("connecting to datastore")
|
// ds.log.SetKV(ds)
|
||||||
|
|
||||||
|
ds.ctx = mrun.OnStart(ds.ctx, func(innerCtx context.Context) error {
|
||||||
|
mlog.Info(ds.ctx, "connecting to datastore")
|
||||||
var err error
|
var err error
|
||||||
ds.Client, err = datastore.NewClient(innerCtx, ds.gce.Project, ds.gce.ClientOptions()...)
|
ds.Client, err = datastore.NewClient(innerCtx, ds.gce.Project, ds.gce.ClientOptions()...)
|
||||||
return merr.WithKV(err, ds.KV())
|
return merr.WithKV(err, ds.KV())
|
||||||
})
|
})
|
||||||
mrun.OnStop(ctx, func(mctx.Context) error {
|
ds.ctx = mrun.OnStop(ds.ctx, func(context.Context) error {
|
||||||
return ds.Client.Close()
|
return ds.Client.Close()
|
||||||
})
|
})
|
||||||
return ds
|
return mctx.WithChild(ctx, ds.ctx), ds
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
// KV implements the mlog.KVer interface.
|
||||||
|
@ -12,8 +12,8 @@ import (
|
|||||||
// Requires datastore emulator to be running
|
// Requires datastore emulator to be running
|
||||||
func TestBasic(t *T) {
|
func TestBasic(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
||||||
ds := MNew(ctx, nil)
|
ctx, ds := MNew(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
name := mrand.Hex(8)
|
name := mrand.Hex(8)
|
||||||
key := datastore.NameKey("testKind", name, nil)
|
key := datastore.NameKey("testKind", name, nil)
|
||||||
|
21
mdb/mdb.go
21
mdb/mdb.go
@ -3,6 +3,8 @@
|
|||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
@ -18,27 +20,28 @@ type GCE struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MGCE returns a GCE instance which will be initialized and configured when the
|
// MGCE returns a GCE instance which will be initialized and configured when the
|
||||||
// start event is triggered on ctx (see mrun.Start). defaultProject is used as
|
// start event is triggered on the returned Context (see mrun.Start).
|
||||||
// the default value for the mcfg parameter this function creates.
|
// defaultProject is used as the default value for the mcfg parameter this
|
||||||
func MGCE(ctx mctx.Context, defaultProject string) *GCE {
|
// function creates.
|
||||||
ctx = mctx.ChildOf(ctx, "gce")
|
func MGCE(parent context.Context, defaultProject string) (context.Context, *GCE) {
|
||||||
credFile := mcfg.String(ctx, "cred-file", "", "Path to GCE credientials JSON file, if any")
|
ctx := mctx.NewChild(parent, "gce")
|
||||||
|
ctx, credFile := mcfg.String(ctx, "cred-file", "", "Path to GCE credientials JSON file, if any")
|
||||||
|
|
||||||
var project *string
|
var project *string
|
||||||
const projectUsage = "Name of GCE project to use"
|
const projectUsage = "Name of GCE project to use"
|
||||||
if defaultProject == "" {
|
if defaultProject == "" {
|
||||||
project = mcfg.RequiredString(ctx, "project", projectUsage)
|
ctx, project = mcfg.RequiredString(ctx, "project", projectUsage)
|
||||||
} else {
|
} else {
|
||||||
project = mcfg.String(ctx, "project", defaultProject, projectUsage)
|
ctx, project = mcfg.String(ctx, "project", defaultProject, projectUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
var gce GCE
|
var gce GCE
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
||||||
gce.Project = *project
|
gce.Project = *project
|
||||||
gce.CredFile = *credFile
|
gce.CredFile = *credFile
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return &gce
|
return mctx.WithChild(parent, ctx), &gce
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientOptions generates and returns the ClientOption instances which can be
|
// ClientOptions generates and returns the ClientOption instances which can be
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO this package still uses context.Context in the callback functions
|
|
||||||
// TODO Consume (and probably BatchConsume) don't properly handle the Client
|
// TODO Consume (and probably BatchConsume) don't properly handle the Client
|
||||||
// being closed.
|
// being closed.
|
||||||
|
|
||||||
@ -39,38 +38,39 @@ type PubSub struct {
|
|||||||
*pubsub.Client
|
*pubsub.Client
|
||||||
|
|
||||||
gce *mdb.GCE
|
gce *mdb.GCE
|
||||||
log *mlog.Logger
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a PubSub instance which will be initialized and configured when
|
// MNew returns a PubSub instance which will be initialized and configured when
|
||||||
// the start event is triggered on ctx (see mrun.Start). The PubSub instance
|
// the start event is triggered on the returned Context (see mrun.Start). The
|
||||||
// will have Close called on it when the stop event is triggered on ctx (see
|
// PubSub instance will have Close called on it when the stop event is triggered
|
||||||
// mrun.Stop).
|
// on the returned Context(see mrun.Stop).
|
||||||
//
|
//
|
||||||
// gce is optional and can be passed in if there's an existing gce object which
|
// gce is optional and can be passed in if there's an existing gce object which
|
||||||
// should be used, otherwise a new one will be created with mdb.MGCE.
|
// should be used, otherwise a new one will be created with mdb.MGCE.
|
||||||
func MNew(ctx mctx.Context, gce *mdb.GCE) *PubSub {
|
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *PubSub) {
|
||||||
if gce == nil {
|
if gce == nil {
|
||||||
gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.MGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = mctx.ChildOf(ctx, "pubsub")
|
|
||||||
ps := &PubSub{
|
ps := &PubSub{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
log: mlog.From(ctx),
|
ctx: mctx.NewChild(ctx, "pubsub"),
|
||||||
}
|
}
|
||||||
ps.log.SetKV(ps)
|
|
||||||
|
|
||||||
mrun.OnStart(ctx, func(innerCtx mctx.Context) error {
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
ps.log.Info("connecting to pubsub")
|
// ps.log.SetKV(ps)
|
||||||
|
|
||||||
|
ps.ctx = mrun.OnStart(ps.ctx, func(innerCtx context.Context) error {
|
||||||
|
mlog.Info(ps.ctx, "connecting to pubsub")
|
||||||
var err error
|
var err error
|
||||||
ps.Client, err = pubsub.NewClient(innerCtx, ps.gce.Project, ps.gce.ClientOptions()...)
|
ps.Client, err = pubsub.NewClient(innerCtx, ps.gce.Project, ps.gce.ClientOptions()...)
|
||||||
return merr.WithKV(err, ps.KV())
|
return merr.WithKV(err, ps.KV())
|
||||||
})
|
})
|
||||||
mrun.OnStop(ctx, func(mctx.Context) error {
|
ps.ctx = mrun.OnStop(ps.ctx, func(context.Context) error {
|
||||||
return ps.Client.Close()
|
return ps.Client.Close()
|
||||||
})
|
})
|
||||||
return ps
|
return mctx.WithChild(ctx, ps.ctx), ps
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface
|
// KV implements the mlog.KVer interface
|
||||||
@ -226,7 +226,7 @@ func (s *Subscription) Consume(ctx context.Context, fn ConsumerFunc, opts Consum
|
|||||||
|
|
||||||
ok, err := fn(context.Context(innerCtx), msg)
|
ok, err := fn(context.Context(innerCtx), msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.topic.ps.log.Warn("error consuming pubsub message", s, merr.KV(err))
|
mlog.Warn(s.topic.ps.ctx, "error consuming pubsub message", s, merr.KV(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
@ -238,7 +238,7 @@ func (s *Subscription) Consume(ctx context.Context, fn ConsumerFunc, opts Consum
|
|||||||
if octx.Err() == context.Canceled || err == nil {
|
if octx.Err() == context.Canceled || err == nil {
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
s.topic.ps.log.Warn("error consuming from pubsub", s, merr.KV(err))
|
mlog.Warn(s.topic.ps.ctx, "error consuming from pubsub", s, merr.KV(err))
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,7 +331,7 @@ func (s *Subscription) BatchConsume(
|
|||||||
}
|
}
|
||||||
ret, err := fn(thisCtx, msgs)
|
ret, err := fn(thisCtx, msgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.topic.ps.log.Warn("error consuming pubsub batch messages", s, merr.KV(err))
|
mlog.Warn(s.topic.ps.ctx, "error consuming pubsub batch messages", s, merr.KV(err))
|
||||||
}
|
}
|
||||||
for i := range thisGroup {
|
for i := range thisGroup {
|
||||||
thisGroup[i].retCh <- ret // retCh is buffered
|
thisGroup[i].retCh <- ret // retCh is buffered
|
||||||
|
@ -14,8 +14,8 @@ import (
|
|||||||
// this requires the pubsub emulator to be running
|
// this requires the pubsub emulator to be running
|
||||||
func TestPubSub(t *T) {
|
func TestPubSub(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
||||||
ps := MNew(ctx, nil)
|
ctx, ps := MNew(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
topicName := "testTopic_" + mrand.Hex(8)
|
topicName := "testTopic_" + mrand.Hex(8)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@ -48,8 +48,8 @@ func TestPubSub(t *T) {
|
|||||||
|
|
||||||
func TestBatchPubSub(t *T) {
|
func TestBatchPubSub(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
||||||
ps := MNew(ctx, nil)
|
ctx, ps := MNew(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
|
|
||||||
topicName := "testBatchTopic_" + mrand.Hex(8)
|
topicName := "testBatchTopic_" + mrand.Hex(8)
|
||||||
|
@ -17,27 +17,39 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MServer is returned by MListenAndServe and simply wraps an *http.Server.
|
||||||
|
type MServer struct {
|
||||||
|
*http.Server
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
// MListenAndServe returns an http.Server which will be initialized and have
|
// MListenAndServe returns an http.Server which will be initialized and have
|
||||||
// ListenAndServe called on it (asynchronously) when the start event is
|
// ListenAndServe called on it (asynchronously) when the start event is
|
||||||
// triggered on ctx (see mrun.Start). The Server will have Shutdown called on it
|
// triggered on the returned Context (see mrun.Start). The Server will have
|
||||||
// when the stop event is triggered on ctx (see mrun.Stop).
|
// Shutdown called on it when the stop event is triggered on the returned
|
||||||
|
// Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// This function automatically handles setting up configuration parameters via
|
// This function automatically handles setting up configuration parameters via
|
||||||
// mcfg. The default listen address is ":0".
|
// mcfg. The default listen address is ":0".
|
||||||
func MListenAndServe(ctx mctx.Context, h http.Handler) *http.Server {
|
func MListenAndServe(ctx context.Context, h http.Handler) (context.Context, *MServer) {
|
||||||
ctx = mctx.ChildOf(ctx, "http")
|
srv := &MServer{
|
||||||
listener := mnet.MListen(ctx, "tcp", "")
|
Server: &http.Server{Handler: h},
|
||||||
|
ctx: mctx.NewChild(ctx, "http"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener *mnet.MListener
|
||||||
|
srv.ctx, listener = mnet.MListen(srv.ctx, "tcp", "")
|
||||||
listener.NoCloseOnStop = true // http.Server.Shutdown will do this
|
listener.NoCloseOnStop = true // http.Server.Shutdown will do this
|
||||||
|
|
||||||
logger := mlog.From(ctx)
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
logger.SetKV(listener)
|
//logger := mlog.From(ctx)
|
||||||
|
//logger.SetKV(listener)
|
||||||
|
|
||||||
srv := http.Server{Handler: h}
|
srv.ctx = mrun.OnStart(srv.ctx, func(context.Context) error {
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
|
||||||
srv.Addr = listener.Addr().String()
|
srv.Addr = listener.Addr().String()
|
||||||
mrun.Thread(ctx, func() error {
|
srv.ctx = mrun.Thread(srv.ctx, func() error {
|
||||||
if err := srv.Serve(listener); err != http.ErrServerClosed {
|
if err := srv.Serve(listener); err != http.ErrServerClosed {
|
||||||
logger.Error("error serving listener", merr.KV(err))
|
mlog.Error(srv.ctx, "error serving listener", merr.KV(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -45,15 +57,15 @@ func MListenAndServe(ctx mctx.Context, h http.Handler) *http.Server {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
mrun.OnStop(ctx, func(innerCtx mctx.Context) error {
|
srv.ctx = mrun.OnStop(srv.ctx, func(innerCtx context.Context) error {
|
||||||
logger.Info("shutting down server")
|
mlog.Info(srv.ctx, "shutting down server")
|
||||||
if err := srv.Shutdown(context.Context(innerCtx)); err != nil {
|
if err := srv.Shutdown(innerCtx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return mrun.Wait(ctx, innerCtx.Done())
|
return mrun.Wait(srv.ctx, innerCtx.Done())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &srv
|
return mctx.WithChild(ctx, srv.ctx), srv
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddXForwardedFor populates the X-Forwarded-For header on the Request to
|
// AddXForwardedFor populates the X-Forwarded-For header on the Request to
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
func TestMListenAndServe(t *T) {
|
func TestMListenAndServe(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
|
|
||||||
srv := MListenAndServe(ctx, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ctx, srv := MListenAndServe(ctx, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
io.Copy(rw, r.Body)
|
io.Copy(rw, r.Body)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
110
mlog/ctx.go
110
mlog/ctx.go
@ -1,87 +1,53 @@
|
|||||||
package mlog
|
package mlog
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ctxKey int
|
type ctxKey int
|
||||||
|
|
||||||
// CtxSet caches the Logger in the Context, overwriting any previous one which
|
// Set returns the Context with the Logger carried by it.
|
||||||
// might have been cached there. From is the corresponding function which
|
func Set(ctx context.Context, l *Logger) context.Context {
|
||||||
// retrieves the Logger back out when needed.
|
return context.WithValue(ctx, ctxKey(0), l)
|
||||||
//
|
}
|
||||||
// This function can be used to premptively set a preconfigured Logger on a root
|
|
||||||
// Context so that the default (NewLogger) isn't used when From is called for
|
// DefaultLogger is an instance of Logger which is returned by From when a
|
||||||
// the first time.
|
// Logger hasn't been previously Set on the Context passed in.
|
||||||
func CtxSet(ctx mctx.Context, l *Logger) {
|
var DefaultLogger = NewLogger()
|
||||||
mctx.GetSetMutableValue(ctx, false, ctxKey(0), func(interface{}) interface{} {
|
|
||||||
|
// From returns the Logger carried by this Context, or DefaultLogger if none is
|
||||||
|
// being carried.
|
||||||
|
func From(ctx context.Context) *Logger {
|
||||||
|
if l, _ := ctx.Value(ctxKey(0)).(*Logger); l != nil {
|
||||||
return l
|
return l
|
||||||
})
|
}
|
||||||
|
return DefaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// CtxSetAll traverses the given Context's children, breadth-first. It calls the
|
// Debug is a shortcut for
|
||||||
// callback for each Context which has a Logger set on it, replacing that Logger
|
// mlog.From(ctx).Debug(ctx, descr, kvs...)
|
||||||
// with the returned one.
|
func Debug(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
//
|
From(ctx).Debug(ctx, descr, kvs...)
|
||||||
// This is useful, for example, when changing the log level of all Loggers in an
|
|
||||||
// app.
|
|
||||||
func CtxSetAll(ctx mctx.Context, callback func(mctx.Context, *Logger) *Logger) {
|
|
||||||
mctx.BreadthFirstVisit(ctx, func(ctx mctx.Context) bool {
|
|
||||||
mctx.GetSetMutableValue(ctx, false, ctxKey(0), func(i interface{}) interface{} {
|
|
||||||
if i == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return callback(ctx, i.(*Logger))
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ctxPathStringer struct {
|
// Info is a shortcut for
|
||||||
str Stringer
|
// mlog.From(ctx).Info(ctx, descr, kvs...)
|
||||||
pathStr string
|
func Info(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
|
From(ctx).Info(ctx, descr, kvs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp ctxPathStringer) String() string {
|
// Warn is a shortcut for
|
||||||
return "(" + cp.pathStr + ") " + cp.str.String()
|
// mlog.From(ctx).Warn(ctx, descr, kvs...)
|
||||||
|
func Warn(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
|
From(ctx).Warn(ctx, descr, kvs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From returns an instance of Logger which has been customized for this
|
// Error is a shortcut for
|
||||||
// Context, primarily by adding a prefix describing the Context's path to all
|
// mlog.From(ctx).Error(ctx, descr, kvs...)
|
||||||
// Message descriptions the Logger receives.
|
func Error(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
//
|
From(ctx).Error(ctx, descr, kvs...)
|
||||||
// The Context caches within it the generated Logger, so a new one isn't created
|
}
|
||||||
// everytime. When From is first called on a Context the Logger inherits the
|
|
||||||
// Context parent's Logger. If the parent hasn't had From called on it its
|
// Fatal is a shortcut for
|
||||||
// parent will be queried instead, and so on.
|
// mlog.From(ctx).Fatal(ctx, descr, kvs...)
|
||||||
func From(ctx mctx.Context) *Logger {
|
func Fatal(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
return mctx.GetSetMutableValue(ctx, true, ctxKey(0), func(interface{}) interface{} {
|
From(ctx).Fatal(ctx, descr, kvs...)
|
||||||
ctxPath := mctx.Path(ctx)
|
|
||||||
if len(ctxPath) == 0 {
|
|
||||||
// we're at the root node and it doesn't have a Logger set, use
|
|
||||||
// the default
|
|
||||||
return NewLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up child's logger
|
|
||||||
pathStr := "/" + path.Join(ctxPath...)
|
|
||||||
|
|
||||||
parentL := From(mctx.Parent(ctx))
|
|
||||||
parentH := parentL.Handler()
|
|
||||||
thisL := parentL.Clone()
|
|
||||||
thisL.SetHandler(func(msg Message) error {
|
|
||||||
// if the Description is already a ctxPathStringer it can be
|
|
||||||
// assumed this Message was passed in from a child Logger.
|
|
||||||
if _, ok := msg.Description.(ctxPathStringer); !ok {
|
|
||||||
msg.Description = ctxPathStringer{
|
|
||||||
str: msg.Description,
|
|
||||||
pathStr: pathStr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parentH(msg)
|
|
||||||
})
|
|
||||||
return thisL
|
|
||||||
}).(*Logger)
|
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,67 @@
|
|||||||
package mlog
|
package mlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContextStuff(t *T) {
|
func TestContextLogging(t *T) {
|
||||||
ctx := mctx.New()
|
|
||||||
ctx1 := mctx.ChildOf(ctx, "1")
|
|
||||||
ctx1a := mctx.ChildOf(ctx1, "a")
|
|
||||||
ctx1b := mctx.ChildOf(ctx1, "b")
|
|
||||||
|
|
||||||
var descrs []string
|
var lines []string
|
||||||
l := NewLogger()
|
l := NewLogger()
|
||||||
l.SetHandler(func(msg Message) error {
|
l.SetHandler(func(msg Message) error {
|
||||||
descrs = append(descrs, msg.Description.String())
|
buf := new(bytes.Buffer)
|
||||||
|
if err := DefaultFormat(buf, msg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lines = append(lines, strings.TrimSuffix(buf.String(), "\n"))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
CtxSet(ctx, l)
|
|
||||||
|
|
||||||
From(ctx1a).Info("ctx1a")
|
ctx := Set(context.Background(), l)
|
||||||
From(ctx1).Info("ctx1")
|
ctx1 := mctx.NewChild(ctx, "1")
|
||||||
From(ctx).Info("ctx")
|
ctx1a := mctx.NewChild(ctx1, "a")
|
||||||
From(ctx1b).Debug("ctx1b (shouldn't show up)")
|
ctx1b := mctx.NewChild(ctx1, "b")
|
||||||
From(ctx1b).Info("ctx1b")
|
ctx1 = mctx.WithChild(ctx1, ctx1a)
|
||||||
|
ctx1 = mctx.WithChild(ctx1, ctx1b)
|
||||||
|
ctx = mctx.WithChild(ctx, ctx1)
|
||||||
|
|
||||||
ctx2 := mctx.ChildOf(ctx, "2")
|
From(ctx).Info(ctx1a, "ctx1a")
|
||||||
From(ctx2).Info("ctx2")
|
From(ctx).Info(ctx1, "ctx1")
|
||||||
|
From(ctx).Info(ctx, "ctx")
|
||||||
|
From(ctx).Debug(ctx1b, "ctx1b (shouldn't show up)")
|
||||||
|
From(ctx).Info(ctx1b, "ctx1b")
|
||||||
|
|
||||||
|
ctx2 := mctx.NewChild(ctx, "2")
|
||||||
|
ctx = mctx.WithChild(ctx, ctx2)
|
||||||
|
From(ctx2).Info(ctx2, "ctx2")
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(descrs, 5),
|
massert.Len(lines, 5),
|
||||||
massert.Equal(descrs[0], "(/1/a) ctx1a"),
|
massert.Equal(lines[0], "~ INFO -- (/1/a) ctx1a"),
|
||||||
massert.Equal(descrs[1], "(/1) ctx1"),
|
massert.Equal(lines[1], "~ INFO -- (/1) ctx1"),
|
||||||
massert.Equal(descrs[2], "ctx"),
|
massert.Equal(lines[2], "~ INFO -- ctx"),
|
||||||
massert.Equal(descrs[3], "(/1/b) ctx1b"),
|
massert.Equal(lines[3], "~ INFO -- (/1/b) ctx1b"),
|
||||||
massert.Equal(descrs[4], "(/2) ctx2"),
|
massert.Equal(lines[4], "~ INFO -- (/2) ctx2"),
|
||||||
))
|
))
|
||||||
|
|
||||||
// use CtxSetAll to change all MaxLevels in-place
|
// changing MaxLevel on ctx's Logger should change it for all
|
||||||
ctx2L := From(ctx2)
|
From(ctx).SetMaxLevel(DebugLevel)
|
||||||
CtxSetAll(ctx, func(_ mctx.Context, l *Logger) *Logger {
|
|
||||||
l.SetMaxLevel(DebugLevel)
|
|
||||||
return l
|
|
||||||
})
|
|
||||||
|
|
||||||
descrs = descrs[:0]
|
lines = lines[:0]
|
||||||
From(ctx).Info("ctx")
|
From(ctx).Info(ctx, "ctx")
|
||||||
From(ctx).Debug("ctx debug")
|
From(ctx).Debug(ctx, "ctx debug")
|
||||||
ctx2L.Debug("ctx2L debug")
|
From(ctx2).Debug(ctx2, "ctx2 debug")
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(descrs, 3),
|
massert.Len(lines, 3),
|
||||||
massert.Equal(descrs[0], "ctx"),
|
massert.Equal(lines[0], "~ INFO -- ctx"),
|
||||||
massert.Equal(descrs[1], "ctx debug"),
|
massert.Equal(lines[1], "~ DEBUG -- ctx debug"),
|
||||||
massert.Equal(descrs[2], "(/2) ctx2L debug"),
|
massert.Equal(lines[2], "~ DEBUG -- (/2) ctx2 debug"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
50
mlog/mlog.go
50
mlog/mlog.go
@ -17,6 +17,7 @@ package mlog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -25,6 +26,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -204,23 +206,12 @@ func Prefix(kv KVer, prefix string) KVer {
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Stringer generates and returns a string.
|
|
||||||
type Stringer interface {
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// String is simply a string which implements Stringer.
|
|
||||||
type String string
|
|
||||||
|
|
||||||
func (str String) String() string {
|
|
||||||
return string(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message describes a message to be logged, after having already resolved the
|
// Message describes a message to be logged, after having already resolved the
|
||||||
// KVer
|
// KVer
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
context.Context
|
||||||
Level
|
Level
|
||||||
Description Stringer
|
Description string
|
||||||
KVer
|
KVer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +244,11 @@ func DefaultFormat(w io.Writer, msg Message) error {
|
|||||||
_, err = fmt.Fprintf(w, s, args...)
|
_, err = fmt.Fprintf(w, s, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
write("~ %s -- %s", msg.Level.String(), msg.Description.String())
|
write("~ %s -- ", msg.Level.String())
|
||||||
|
if path := mctx.Path(msg.Context); len(path) > 0 {
|
||||||
|
write("(%s) ", "/"+strings.Join(path, "/"))
|
||||||
|
}
|
||||||
|
write("%s", msg.Description)
|
||||||
if msg.KVer != nil {
|
if msg.KVer != nil {
|
||||||
if kv := msg.KV(); len(kv) > 0 {
|
if kv := msg.KV(); len(kv) > 0 {
|
||||||
write(" --")
|
write(" --")
|
||||||
@ -363,7 +358,7 @@ func (l *Logger) Log(msg Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := l.h(msg); err != nil {
|
if err := l.h(msg); err != nil {
|
||||||
go l.Error("Logger.Handler returned error", merr.KV(err))
|
go l.Error(context.Background(), "Logger.Handler returned error", merr.KV(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,36 +371,37 @@ func (l *Logger) Log(msg Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkMsg(lvl Level, descr string, kvs ...KVer) Message {
|
func mkMsg(ctx context.Context, lvl Level, descr string, kvs ...KVer) Message {
|
||||||
return Message{
|
return Message{
|
||||||
|
Context: ctx,
|
||||||
Level: lvl,
|
Level: lvl,
|
||||||
Description: String(descr),
|
Description: descr,
|
||||||
KVer: Merge(kvs...),
|
KVer: Merge(kvs...),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a DebugLevel message, merging the KVers together first
|
// Debug logs a DebugLevel message, merging the KVers together first
|
||||||
func (l *Logger) Debug(descr string, kvs ...KVer) {
|
func (l *Logger) Debug(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
l.Log(mkMsg(DebugLevel, descr, kvs...))
|
l.Log(mkMsg(ctx, DebugLevel, descr, kvs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a InfoLevel message, merging the KVers together first
|
// Info logs a InfoLevel message, merging the KVers together first
|
||||||
func (l *Logger) Info(descr string, kvs ...KVer) {
|
func (l *Logger) Info(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
l.Log(mkMsg(InfoLevel, descr, kvs...))
|
l.Log(mkMsg(ctx, InfoLevel, descr, kvs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a WarnLevel message, merging the KVers together first
|
// Warn logs a WarnLevel message, merging the KVers together first
|
||||||
func (l *Logger) Warn(descr string, kvs ...KVer) {
|
func (l *Logger) Warn(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
l.Log(mkMsg(WarnLevel, descr, kvs...))
|
l.Log(mkMsg(ctx, WarnLevel, descr, kvs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a ErrorLevel message, merging the KVers together first
|
// Error logs a ErrorLevel message, merging the KVers together first
|
||||||
func (l *Logger) Error(descr string, kvs ...KVer) {
|
func (l *Logger) Error(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
l.Log(mkMsg(ErrorLevel, descr, kvs...))
|
l.Log(mkMsg(ctx, ErrorLevel, descr, kvs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a FatalLevel message, merging the KVers together first. A Fatal
|
// Fatal logs a FatalLevel message, merging the KVers together first. A Fatal
|
||||||
// message automatically stops the process with an os.Exit(1)
|
// message automatically stops the process with an os.Exit(1)
|
||||||
func (l *Logger) Fatal(descr string, kvs ...KVer) {
|
func (l *Logger) Fatal(ctx context.Context, descr string, kvs ...KVer) {
|
||||||
l.Log(mkMsg(FatalLevel, descr, kvs...))
|
l.Log(mkMsg(ctx, FatalLevel, descr, kvs...))
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package mlog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
@ -36,6 +37,7 @@ func TestKV(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogger(t *T) {
|
func TestLogger(t *T) {
|
||||||
|
ctx := context.Background()
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
h := func(msg Message) error {
|
h := func(msg Message) error {
|
||||||
return DefaultFormat(buf, msg)
|
return DefaultFormat(buf, msg)
|
||||||
@ -59,10 +61,10 @@ func TestLogger(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default max level should be INFO
|
// Default max level should be INFO
|
||||||
l.Debug("foo")
|
l.Debug(ctx, "foo")
|
||||||
l.Info("bar")
|
l.Info(ctx, "bar")
|
||||||
l.Warn("baz")
|
l.Warn(ctx, "baz")
|
||||||
l.Error("buz")
|
l.Error(ctx, "buz")
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ INFO -- bar\n"),
|
assertOut("~ INFO -- bar\n"),
|
||||||
assertOut("~ WARN -- baz\n"),
|
assertOut("~ WARN -- baz\n"),
|
||||||
@ -70,10 +72,10 @@ func TestLogger(t *T) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
l.SetMaxLevel(WarnLevel)
|
l.SetMaxLevel(WarnLevel)
|
||||||
l.Debug("foo")
|
l.Debug(ctx, "foo")
|
||||||
l.Info("bar")
|
l.Info(ctx, "bar")
|
||||||
l.Warn("baz")
|
l.Warn(ctx, "baz")
|
||||||
l.Error("buz", KV{"a": "b"})
|
l.Error(ctx, "buz", KV{"a": "b"})
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ WARN -- baz\n"),
|
assertOut("~ WARN -- baz\n"),
|
||||||
assertOut("~ ERROR -- buz -- a=\"b\"\n"),
|
assertOut("~ ERROR -- buz -- a=\"b\"\n"),
|
||||||
@ -82,12 +84,12 @@ func TestLogger(t *T) {
|
|||||||
l2 := l.Clone()
|
l2 := l.Clone()
|
||||||
l2.SetMaxLevel(InfoLevel)
|
l2.SetMaxLevel(InfoLevel)
|
||||||
l2.SetHandler(func(msg Message) error {
|
l2.SetHandler(func(msg Message) error {
|
||||||
msg.Description = String(strings.ToUpper(msg.Description.String()))
|
msg.Description = strings.ToUpper(msg.Description)
|
||||||
return h(msg)
|
return h(msg)
|
||||||
})
|
})
|
||||||
l2.Info("bar")
|
l2.Info(ctx, "bar")
|
||||||
l2.Warn("baz")
|
l2.Warn(ctx, "baz")
|
||||||
l.Error("buz")
|
l.Error(ctx, "buz")
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ INFO -- BAR\n"),
|
assertOut("~ INFO -- BAR\n"),
|
||||||
assertOut("~ WARN -- BAZ\n"),
|
assertOut("~ WARN -- BAZ\n"),
|
||||||
@ -96,8 +98,8 @@ func TestLogger(t *T) {
|
|||||||
|
|
||||||
l3 := l2.Clone()
|
l3 := l2.Clone()
|
||||||
l3.SetKV(KV{"a": 1})
|
l3.SetKV(KV{"a": 1})
|
||||||
l3.Info("foo", KV{"b": 2})
|
l3.Info(ctx, "foo", KV{"b": 2})
|
||||||
l3.Info("bar", KV{"a": 2, "b": 3})
|
l3.Info(ctx, "bar", KV{"a": 2, "b": 3})
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ INFO -- FOO -- a=\"1\" b=\"2\"\n"),
|
assertOut("~ INFO -- FOO -- a=\"1\" b=\"2\"\n"),
|
||||||
assertOut("~ INFO -- BAR -- a=\"2\" b=\"3\"\n"),
|
assertOut("~ INFO -- BAR -- a=\"2\" b=\"3\"\n"),
|
||||||
@ -121,7 +123,11 @@ func TestDefaultFormat(t *T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := Message{Level: InfoLevel, Description: String("this is a test")}
|
msg := Message{
|
||||||
|
Context: context.Background(),
|
||||||
|
Level: InfoLevel,
|
||||||
|
Description: "this is a test",
|
||||||
|
}
|
||||||
massert.Fatal(t, assertFormat("INFO -- this is a test", msg))
|
massert.Fatal(t, assertFormat("INFO -- this is a test", msg))
|
||||||
|
|
||||||
msg.KVer = KV{}
|
msg.KVer = KV{}
|
||||||
|
35
mnet/mnet.go
35
mnet/mnet.go
@ -3,11 +3,11 @@
|
|||||||
package mnet
|
package mnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
// MListener is returned by MListen and simply wraps a net.Listener.
|
// MListener is returned by MListen and simply wraps a net.Listener.
|
||||||
type MListener struct {
|
type MListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
log *mlog.Logger
|
ctx context.Context
|
||||||
|
|
||||||
// If set to true before mrun's stop event is run, the stop event will not
|
// If set to true before mrun's stop event is run, the stop event will not
|
||||||
// cause the MListener to be closed.
|
// cause the MListener to be closed.
|
||||||
@ -24,44 +24,47 @@ type MListener struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MListen returns an MListener which will be initialized when the start event
|
// MListen returns an MListener which will be initialized when the start event
|
||||||
// is triggered on ctx (see mrun.Start), and closed when the stop event is
|
// is triggered on the returned Context (see mrun.Start), and closed when the
|
||||||
// triggered on ctx (see mrun.Stop).
|
// stop event is triggered on the returned Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// network defaults to "tcp" if empty. defaultAddr defaults to ":0" if empty,
|
// network defaults to "tcp" if empty. defaultAddr defaults to ":0" if empty,
|
||||||
// and will be configurable via mcfg.
|
// and will be configurable via mcfg.
|
||||||
func MListen(ctx mctx.Context, network, defaultAddr string) *MListener {
|
func MListen(ctx context.Context, network, defaultAddr string) (context.Context, *MListener) {
|
||||||
if network == "" {
|
if network == "" {
|
||||||
network = "tcp"
|
network = "tcp"
|
||||||
}
|
}
|
||||||
if defaultAddr == "" {
|
if defaultAddr == "" {
|
||||||
defaultAddr = ":0"
|
defaultAddr = ":0"
|
||||||
}
|
}
|
||||||
addr := mcfg.String(ctx, "listen-addr", defaultAddr, strings.ToUpper(network)+" address to listen on in format [host]:port. If port is 0 then a random one will be chosen")
|
|
||||||
|
|
||||||
l := new(MListener)
|
l := new(MListener)
|
||||||
l.log = mlog.From(ctx)
|
|
||||||
l.log.SetKV(l)
|
|
||||||
|
|
||||||
mrun.OnStart(ctx, func(mctx.Context) error {
|
// TODO the equivalent functionality as here will be added with annotations
|
||||||
|
//l.log = mlog.From(ctx)
|
||||||
|
//l.log.SetKV(l)
|
||||||
|
|
||||||
|
ctx, addr := mcfg.String(ctx, "listen-addr", defaultAddr, strings.ToUpper(network)+" address to listen on in format [host]:port. If port is 0 then a random one will be chosen")
|
||||||
|
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
if l.Listener, err = net.Listen(network, *addr); err != nil {
|
if l.Listener, err = net.Listen(network, *addr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
l.log.Info("listening")
|
mlog.Info(l.ctx, "listening")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO track connections and wait for them to complete before shutting
|
// TODO track connections and wait for them to complete before shutting
|
||||||
// down?
|
// down?
|
||||||
mrun.OnStop(ctx, func(mctx.Context) error {
|
ctx = mrun.OnStop(ctx, func(context.Context) error {
|
||||||
if l.NoCloseOnStop {
|
if l.NoCloseOnStop {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
l.log.Info("stopping listener")
|
mlog.Info(l.ctx, "stopping listener")
|
||||||
return l.Close()
|
return l.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
return l
|
l.ctx = ctx
|
||||||
|
return ctx, l
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept wraps a call to Accept on the underlying net.Listener, providing debug
|
// Accept wraps a call to Accept on the underlying net.Listener, providing debug
|
||||||
@ -71,16 +74,16 @@ func (l *MListener) Accept() (net.Conn, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
l.log.Debug("connection accepted", mlog.KV{"remoteAddr": conn.RemoteAddr()})
|
mlog.Debug(l.ctx, "connection accepted", mlog.KV{"remoteAddr": conn.RemoteAddr()})
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close wraps a call to Close on the underlying net.Listener, providing debug
|
// Close wraps a call to Close on the underlying net.Listener, providing debug
|
||||||
// logging.
|
// logging.
|
||||||
func (l *MListener) Close() error {
|
func (l *MListener) Close() error {
|
||||||
l.log.Debug("listener closing")
|
mlog.Debug(l.ctx, "listener closing")
|
||||||
err := l.Listener.Close()
|
err := l.Listener.Close()
|
||||||
l.log.Debug("listener closed", merr.KV(err))
|
mlog.Debug(l.ctx, "listener closed", merr.KV(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ func TestIsReservedIP(t *T) {
|
|||||||
|
|
||||||
func TestMListen(t *T) {
|
func TestMListen(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.NewCtx()
|
||||||
l := MListen(ctx, "", "")
|
ctx, l := MListen(ctx, "", "")
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := net.Dial("tcp", l.Addr().String())
|
conn, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
182
mrun/hook.go
182
mrun/hook.go
@ -1,87 +1,143 @@
|
|||||||
package mrun
|
package mrun
|
||||||
|
|
||||||
import "github.com/mediocregopher/mediocre-go-lib/mctx"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
type ctxEventKeyWrap struct {
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
key interface{}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Hook describes a function which can be registered to trigger on an event via
|
// Hook describes a function which can be registered to trigger on an event via
|
||||||
// the RegisterHook function.
|
// the RegisterHook function.
|
||||||
type Hook func(mctx.Context) error
|
type Hook func(context.Context) error
|
||||||
|
|
||||||
|
type ctxKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxKeyHookEls ctxKey = iota
|
||||||
|
ctxKeyNumChildren
|
||||||
|
ctxKeyNumHooks
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKeyWrap struct {
|
||||||
|
key ctxKey
|
||||||
|
userKey interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// because we want Hooks to be called in the order created, taking into account
|
||||||
|
// the creation of children and their hooks as well, we create a sequence of
|
||||||
|
// elements which can either be a Hook or a child.
|
||||||
|
type hookEl struct {
|
||||||
|
hook Hook
|
||||||
|
child context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxKeys(userKey interface{}) (ctxKeyWrap, ctxKeyWrap, ctxKeyWrap) {
|
||||||
|
return ctxKeyWrap{
|
||||||
|
key: ctxKeyHookEls,
|
||||||
|
userKey: userKey,
|
||||||
|
}, ctxKeyWrap{
|
||||||
|
key: ctxKeyNumChildren,
|
||||||
|
userKey: userKey,
|
||||||
|
}, ctxKeyWrap{
|
||||||
|
key: ctxKeyNumHooks,
|
||||||
|
userKey: userKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHookEls retrieves a copy of the []hookEl in the Context and possibly
|
||||||
|
// appends more elements if more children have been added since that []hookEl
|
||||||
|
// was created.
|
||||||
|
//
|
||||||
|
// this also returns the latest numChildren and numHooks values for convenience.
|
||||||
|
func getHookEls(ctx context.Context, userKey interface{}) ([]hookEl, int, int) {
|
||||||
|
hookElsKey, numChildrenKey, numHooksKey := ctxKeys(userKey)
|
||||||
|
lastNumChildren, _ := mctx.LocalValue(ctx, numChildrenKey).(int)
|
||||||
|
lastNumHooks, _ := mctx.LocalValue(ctx, numHooksKey).(int)
|
||||||
|
lastHookEls, _ := mctx.LocalValue(ctx, hookElsKey).([]hookEl)
|
||||||
|
children := mctx.Children(ctx)
|
||||||
|
|
||||||
|
// plus 1 in case we wanna append something else outside this function
|
||||||
|
hookEls := make([]hookEl, len(lastHookEls), lastNumHooks+len(children)-lastNumChildren+1)
|
||||||
|
copy(hookEls, lastHookEls)
|
||||||
|
for _, child := range children[lastNumChildren:] {
|
||||||
|
hookEls = append(hookEls, hookEl{child: child})
|
||||||
|
}
|
||||||
|
return hookEls, len(children), lastNumHooks
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterHook registers a Hook under a typed key. The Hook will be called when
|
// RegisterHook registers a Hook under a typed key. The Hook will be called when
|
||||||
// TriggerHooks is called with that same key. Multiple Hooks can be registered
|
// TriggerHooks is called with that same key. Multiple Hooks can be registered
|
||||||
// for the same key, and will be called sequentially when triggered.
|
// for the same key, and will be called sequentially when triggered.
|
||||||
//
|
//
|
||||||
// RegisterHook registers Hooks onto the root of the given Context. Therefore,
|
|
||||||
// Hooks will be triggered in the global order they were registered. For
|
|
||||||
// example: if one Hook is registered on a Context, then one is registered on a
|
|
||||||
// child of that Context, then another one is registered on the original Context
|
|
||||||
// again, the three Hooks will be triggered in the order: parent, child,
|
|
||||||
// parent.
|
|
||||||
//
|
|
||||||
// Hooks will be called with whatever Context is passed into TriggerHooks.
|
// Hooks will be called with whatever Context is passed into TriggerHooks.
|
||||||
func RegisterHook(ctx mctx.Context, key interface{}, hook Hook) {
|
func RegisterHook(ctx context.Context, key interface{}, hook Hook) context.Context {
|
||||||
ctx = mctx.Root(ctx)
|
hookEls, numChildren, numHooks := getHookEls(ctx, key)
|
||||||
mctx.GetSetMutableValue(ctx, false, ctxEventKeyWrap{key}, func(v interface{}) interface{} {
|
hookEls = append(hookEls, hookEl{hook: hook})
|
||||||
hooks, _ := v.([]Hook)
|
|
||||||
return append(hooks, hook)
|
hookElsKey, numChildrenKey, numHooksKey := ctxKeys(key)
|
||||||
})
|
ctx = mctx.WithLocalValue(ctx, hookElsKey, hookEls)
|
||||||
|
ctx = mctx.WithLocalValue(ctx, numChildrenKey, numChildren)
|
||||||
|
ctx = mctx.WithLocalValue(ctx, numHooksKey, numHooks+1)
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerHooks(ctx mctx.Context, key interface{}, next func([]Hook) (Hook, []Hook)) error {
|
func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl) (hookEl, []hookEl)) error {
|
||||||
rootCtx := mctx.Root(ctx)
|
hookEls, _, _ := getHookEls(ctx, userKey)
|
||||||
var err error
|
var hookEl hookEl
|
||||||
mctx.GetSetMutableValue(rootCtx, false, ctxEventKeyWrap{key}, func(i interface{}) interface{} {
|
for {
|
||||||
var hook Hook
|
if len(hookEls) == 0 {
|
||||||
hooks, _ := i.([]Hook)
|
break
|
||||||
for {
|
|
||||||
if len(hooks) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hook, hooks = next(hooks)
|
|
||||||
|
|
||||||
// err here is the var outside GetSetMutableValue, we lift it out
|
|
||||||
if err = hook(ctx); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
hookEl, hookEls = next(hookEls)
|
||||||
// if there was an error then we want to keep all the hooks which
|
if hookEl.child != nil {
|
||||||
// weren't called. If there wasn't we want to reset the value to nil so
|
if err := triggerHooks(hookEl.child, userKey, next); err != nil {
|
||||||
// the slice doesn't grow unbounded.
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return hooks
|
} else if err := hookEl.hook(ctx); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
})
|
return nil
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriggerHooks causes all Hooks registered with RegisterHook under the given
|
// TriggerHooks causes all Hooks registered with RegisterHook on the Context
|
||||||
// key to be called in the global order they were registered, using the given
|
// (and its predecessors) under the given key to be called in the order they
|
||||||
// Context as their input parameter. The given Context does not need to be the
|
// were registered.
|
||||||
// root Context (see RegisterHook).
|
|
||||||
//
|
//
|
||||||
// If any Hook returns an error no further Hooks will be called and that error
|
// If any Hook returns an error no further Hooks will be called and that error
|
||||||
// will be returned.
|
// will be returned.
|
||||||
//
|
//
|
||||||
// TriggerHooks causes all Hooks which were called to be de-registered. If an
|
// If the Context has children (see the mctx package), and those children have
|
||||||
// error caused execution to stop prematurely then any Hooks which were not
|
// Hooks registered under this key, then their Hooks will be called in the
|
||||||
// called will remain registered.
|
// expected order. For example:
|
||||||
func TriggerHooks(ctx mctx.Context, key interface{}) error {
|
//
|
||||||
return triggerHooks(ctx, key, func(hooks []Hook) (Hook, []Hook) {
|
// // parent context has hookA registered
|
||||||
return hooks[0], hooks[1:]
|
// ctx := context.Background()
|
||||||
|
// ctx = RegisterHook(ctx, 0, hookA)
|
||||||
|
//
|
||||||
|
// // child context has hookB registered
|
||||||
|
// childCtx := mctx.NewChild(ctx, "child")
|
||||||
|
// childCtx = RegisterHook(childCtx, 0, hookB)
|
||||||
|
// ctx = mctx.WithChild(ctx, childCtx) // needed to link childCtx to ctx
|
||||||
|
//
|
||||||
|
// // parent context has another Hook, hookC, registered
|
||||||
|
// ctx = RegisterHook(ctx, 0, hookC)
|
||||||
|
//
|
||||||
|
// // The Hooks will be triggered in the order: hookA, hookB, then hookC
|
||||||
|
// err := TriggerHooks(ctx, 0)
|
||||||
|
//
|
||||||
|
func TriggerHooks(ctx context.Context, key interface{}) error {
|
||||||
|
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) {
|
||||||
|
return hookEls[0], hookEls[1:]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriggerHooksReverse is the same as TriggerHooks except that registered Hooks
|
// TriggerHooksReverse is the same as TriggerHooks except that registered Hooks
|
||||||
// are called in the reverse order in which they were registered.
|
// are called in the reverse order in which they were registered.
|
||||||
func TriggerHooksReverse(ctx mctx.Context, key interface{}) error {
|
func TriggerHooksReverse(ctx context.Context, key interface{}) error {
|
||||||
return triggerHooks(ctx, key, func(hooks []Hook) (Hook, []Hook) {
|
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) {
|
||||||
last := len(hooks) - 1
|
last := len(hookEls) - 1
|
||||||
return hooks[last], hooks[:last]
|
return hookEls[last], hookEls[:last]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,24 +157,24 @@ const (
|
|||||||
// server) will want to use the Hook only to initialize, and spawn off a
|
// server) will want to use the Hook only to initialize, and spawn off a
|
||||||
// go-routine to do their actual work. Long-lived tasks should set themselves up
|
// go-routine to do their actual work. Long-lived tasks should set themselves up
|
||||||
// to stop on the stop event (see OnStop).
|
// to stop on the stop event (see OnStop).
|
||||||
func OnStart(ctx mctx.Context, hook Hook) {
|
func OnStart(ctx context.Context, hook Hook) context.Context {
|
||||||
RegisterHook(ctx, start, hook)
|
return RegisterHook(ctx, start, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start runs all Hooks registered using OnStart. This is a special case of
|
// Start runs all Hooks registered using OnStart. This is a special case of
|
||||||
// TriggerHooks.
|
// TriggerHooks.
|
||||||
func Start(ctx mctx.Context) error {
|
func Start(ctx context.Context) error {
|
||||||
return TriggerHooks(ctx, start)
|
return TriggerHooks(ctx, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnStop registers the given Hook to run when Stop is called. This is a special
|
// OnStop registers the given Hook to run when Stop is called. This is a special
|
||||||
// case of RegisterHook.
|
// case of RegisterHook.
|
||||||
func OnStop(ctx mctx.Context, hook Hook) {
|
func OnStop(ctx context.Context, hook Hook) context.Context {
|
||||||
RegisterHook(ctx, stop, hook)
|
return RegisterHook(ctx, stop, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop runs all Hooks registered using OnStop in the reverse order in which
|
// Stop runs all Hooks registered using OnStop in the reverse order in which
|
||||||
// they were registered. This is a special case of TriggerHooks.
|
// they were registered. This is a special case of TriggerHooks.
|
||||||
func Stop(ctx mctx.Context) error {
|
func Stop(ctx context.Context) error {
|
||||||
return TriggerHooksReverse(ctx, stop)
|
return TriggerHooksReverse(ctx, stop)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package mrun
|
package mrun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
@ -9,46 +9,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestHooks(t *T) {
|
func TestHooks(t *T) {
|
||||||
ch := make(chan int, 10)
|
var out []int
|
||||||
ctx := mctx.New()
|
|
||||||
ctxChild := mctx.ChildOf(ctx, "child")
|
|
||||||
|
|
||||||
mkHook := func(i int) Hook {
|
mkHook := func(i int) Hook {
|
||||||
return func(mctx.Context) error {
|
return func(context.Context) error {
|
||||||
ch <- i
|
out = append(out, i)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterHook(ctx, 0, mkHook(0))
|
ctx := context.Background()
|
||||||
RegisterHook(ctxChild, 0, mkHook(1))
|
ctx = RegisterHook(ctx, 0, mkHook(1))
|
||||||
RegisterHook(ctx, 0, mkHook(2))
|
ctx = RegisterHook(ctx, 0, mkHook(2))
|
||||||
|
|
||||||
bogusErr := errors.New("bogus error")
|
ctxA := mctx.NewChild(ctx, "a")
|
||||||
RegisterHook(ctxChild, 0, func(mctx.Context) error { return bogusErr })
|
ctxA = RegisterHook(ctxA, 0, mkHook(3))
|
||||||
|
ctxA = RegisterHook(ctxA, 999, mkHook(999)) // different key
|
||||||
|
ctx = mctx.WithChild(ctx, ctxA)
|
||||||
|
|
||||||
RegisterHook(ctx, 0, mkHook(3))
|
ctx = RegisterHook(ctx, 0, mkHook(4))
|
||||||
RegisterHook(ctx, 0, mkHook(4))
|
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
ctxB := mctx.NewChild(ctx, "b")
|
||||||
massert.Equal(bogusErr, TriggerHooks(ctx, 0)),
|
ctxB = RegisterHook(ctxB, 0, mkHook(5))
|
||||||
massert.Equal(0, <-ch),
|
ctxB1 := mctx.NewChild(ctxB, "1")
|
||||||
massert.Equal(1, <-ch),
|
ctxB1 = RegisterHook(ctxB1, 0, mkHook(6))
|
||||||
massert.Equal(2, <-ch),
|
ctxB = mctx.WithChild(ctxB, ctxB1)
|
||||||
))
|
ctx = mctx.WithChild(ctx, ctxB)
|
||||||
|
|
||||||
// after the error the 3 and 4 Hooks should still be registered, but not
|
|
||||||
// called yet.
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
t.Fatal("Hooks should not have been called yet")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Nil(TriggerHooks(ctx, 0)),
|
massert.Nil(TriggerHooks(ctx, 0)),
|
||||||
massert.Equal(3, <-ch),
|
massert.Equal([]int{1, 2, 3, 4, 5, 6}, out),
|
||||||
massert.Equal(4, <-ch),
|
))
|
||||||
|
|
||||||
|
out = nil
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Nil(TriggerHooksReverse(ctx, 0)),
|
||||||
|
massert.Equal([]int{6, 5, 4, 3, 2, 1}, out),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
37
mrun/mrun.go
37
mrun/mrun.go
@ -3,6 +3,7 @@
|
|||||||
package mrun
|
package mrun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
@ -33,26 +34,24 @@ func (fe *futureErr) set(err error) {
|
|||||||
close(fe.doneCh)
|
close(fe.doneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ctxKey int
|
type threadCtxKey int
|
||||||
|
|
||||||
// Thread spawns a go-routine which executes the given function. When the passed
|
// Thread spawns a go-routine which executes the given function. The returned
|
||||||
// in Context is canceled the Context within all threads spawned from it will
|
// Context tracks this go-routine, which can then be passed into the Wait
|
||||||
// be canceled as well.
|
// function to block until the spawned go-routine returns.
|
||||||
//
|
func Thread(ctx context.Context, fn func() error) context.Context {
|
||||||
// See Wait for accompanying functionality.
|
|
||||||
func Thread(ctx mctx.Context, fn func() error) {
|
|
||||||
futErr := newFutureErr()
|
futErr := newFutureErr()
|
||||||
mctx.GetSetMutableValue(ctx, false, ctxKey(0), func(i interface{}) interface{} {
|
oldFutErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr)
|
||||||
futErrs, ok := i.([]*futureErr)
|
futErrs := make([]*futureErr, len(oldFutErrs), len(oldFutErrs)+1)
|
||||||
if !ok {
|
copy(futErrs, oldFutErrs)
|
||||||
futErrs = make([]*futureErr, 0, 1)
|
futErrs = append(futErrs, futErr)
|
||||||
}
|
ctx = context.WithValue(ctx, threadCtxKey(0), futErrs)
|
||||||
return append(futErrs, futErr)
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
futErr.set(fn())
|
futErr.set(fn())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrDone is returned from Wait if cancelCh is closed before all threads have
|
// ErrDone is returned from Wait if cancelCh is closed before all threads have
|
||||||
@ -60,7 +59,7 @@ func Thread(ctx mctx.Context, fn func() error) {
|
|||||||
var ErrDone = errors.New("Wait is done waiting")
|
var ErrDone = errors.New("Wait is done waiting")
|
||||||
|
|
||||||
// Wait blocks until all go-routines spawned using Thread on the passed in
|
// Wait blocks until all go-routines spawned using Thread on the passed in
|
||||||
// Context, and all of its children, have returned. Any number of the threads
|
// Context (and its predecessors) have returned. Any number of the go-routines
|
||||||
// may have returned already when Wait is called.
|
// may have returned already when Wait is called.
|
||||||
//
|
//
|
||||||
// If any of the thread functions returned an error during its runtime Wait will
|
// If any of the thread functions returned an error during its runtime Wait will
|
||||||
@ -71,10 +70,8 @@ var ErrDone = errors.New("Wait is done waiting")
|
|||||||
// this function stops waiting and returns ErrDone.
|
// this function stops waiting and returns ErrDone.
|
||||||
//
|
//
|
||||||
// Wait is safe to call in parallel, and will return the same result if called
|
// Wait is safe to call in parallel, and will return the same result if called
|
||||||
// multiple times in sequence. If new Thread calls have been made since the last
|
// multiple times in sequence.
|
||||||
// Wait call, the results of those calls will be waited upon during subsequent
|
func Wait(ctx context.Context, cancelCh <-chan struct{}) error {
|
||||||
// Wait calls.
|
|
||||||
func Wait(ctx mctx.Context, cancelCh <-chan struct{}) error {
|
|
||||||
// First wait for all the children, and see if any of them return an error
|
// First wait for all the children, and see if any of them return an error
|
||||||
children := mctx.Children(ctx)
|
children := mctx.Children(ctx)
|
||||||
for _, childCtx := range children {
|
for _, childCtx := range children {
|
||||||
@ -83,7 +80,7 @@ func Wait(ctx mctx.Context, cancelCh <-chan struct{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
futErrs, _ := mctx.MutableValue(ctx, ctxKey(0)).([]*futureErr)
|
futErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr)
|
||||||
for _, futErr := range futErrs {
|
for _, futErr := range futErrs {
|
||||||
err, ok := futErr.get(cancelCh)
|
err, ok := futErr.get(cancelCh)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mrun
|
package mrun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
. "testing"
|
. "testing"
|
||||||
"time"
|
"time"
|
||||||
@ -12,11 +13,11 @@ func TestThreadWait(t *T) {
|
|||||||
testErr := errors.New("test error")
|
testErr := errors.New("test error")
|
||||||
|
|
||||||
cancelCh := func(t time.Duration) <-chan struct{} {
|
cancelCh := func(t time.Duration) <-chan struct{} {
|
||||||
tCtx, _ := mctx.WithTimeout(mctx.New(), t*2)
|
tCtx, _ := context.WithTimeout(context.Background(), t*2)
|
||||||
return tCtx.Done()
|
return tCtx.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := func(ctx mctx.Context, shouldTake time.Duration) error {
|
wait := func(ctx context.Context, shouldTake time.Duration) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := Wait(ctx, cancelCh(shouldTake*2))
|
err := Wait(ctx, cancelCh(shouldTake*2))
|
||||||
if took := time.Since(start); took < shouldTake || took > shouldTake*4/3 {
|
if took := time.Since(start); took < shouldTake || took > shouldTake*4/3 {
|
||||||
@ -28,16 +29,16 @@ func TestThreadWait(t *T) {
|
|||||||
t.Run("noChildren", func(t *T) {
|
t.Run("noChildren", func(t *T) {
|
||||||
t.Run("noBlock", func(t *T) {
|
t.Run("noBlock", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Thread(ctx, func() error { return nil })
|
ctx = Thread(ctx, func() error { return nil })
|
||||||
if err := Wait(ctx, nil); err != nil {
|
if err := Wait(ctx, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Thread(ctx, func() error { return testErr })
|
ctx = Thread(ctx, func() error { return testErr })
|
||||||
if err := Wait(ctx, nil); err != testErr {
|
if err := Wait(ctx, nil); err != testErr {
|
||||||
t.Fatalf("should have got test error, got: %v", err)
|
t.Fatalf("should have got test error, got: %v", err)
|
||||||
}
|
}
|
||||||
@ -46,8 +47,8 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("block", func(t *T) {
|
t.Run("block", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Thread(ctx, func() error {
|
ctx = Thread(ctx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -57,8 +58,8 @@ func TestThreadWait(t *T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Thread(ctx, func() error {
|
ctx = Thread(ctx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
@ -68,8 +69,8 @@ func TestThreadWait(t *T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("canceled", func(t *T) {
|
t.Run("canceled", func(t *T) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
Thread(ctx, func() error {
|
ctx = Thread(ctx, func() error {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
@ -80,16 +81,17 @@ func TestThreadWait(t *T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ctxWithChild := func() (mctx.Context, mctx.Context) {
|
ctxWithChild := func() (context.Context, context.Context) {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
return ctx, mctx.ChildOf(ctx, "child")
|
return ctx, mctx.NewChild(ctx, "child")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("children", func(t *T) {
|
t.Run("children", func(t *T) {
|
||||||
t.Run("noBlock", func(t *T) {
|
t.Run("noBlock", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
Thread(childCtx, func() error { return nil })
|
childCtx = Thread(childCtx, func() error { return nil })
|
||||||
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := Wait(ctx, nil); err != nil {
|
if err := Wait(ctx, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -97,7 +99,8 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
Thread(childCtx, func() error { return testErr })
|
childCtx = Thread(childCtx, func() error { return testErr })
|
||||||
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := Wait(ctx, nil); err != testErr {
|
if err := Wait(ctx, nil); err != testErr {
|
||||||
t.Fatalf("should have got test error, got: %v", err)
|
t.Fatalf("should have got test error, got: %v", err)
|
||||||
}
|
}
|
||||||
@ -107,10 +110,11 @@ func TestThreadWait(t *T) {
|
|||||||
t.Run("block", func(t *T) {
|
t.Run("block", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
Thread(childCtx, func() error {
|
childCtx = Thread(childCtx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := wait(ctx, 1*time.Second); err != nil {
|
if err := wait(ctx, 1*time.Second); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -118,10 +122,11 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
Thread(childCtx, func() error {
|
childCtx = Thread(childCtx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := wait(ctx, 1*time.Second); err != testErr {
|
if err := wait(ctx, 1*time.Second); err != testErr {
|
||||||
t.Fatalf("should have got test error, got: %v", err)
|
t.Fatalf("should have got test error, got: %v", err)
|
||||||
}
|
}
|
||||||
@ -129,10 +134,11 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("canceled", func(t *T) {
|
t.Run("canceled", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
Thread(childCtx, func() error {
|
childCtx = Thread(childCtx, func() error {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := Wait(ctx, cancelCh(500*time.Millisecond)); err != ErrDone {
|
if err := Wait(ctx, cancelCh(500*time.Millisecond)); err != ErrDone {
|
||||||
t.Fatalf("should have got ErrDone, got: %v", err)
|
t.Fatalf("should have got ErrDone, got: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2,35 +2,33 @@
|
|||||||
package mtest
|
package mtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ctxKey int
|
type envCtxKey int
|
||||||
|
|
||||||
// NewCtx creates and returns a root Context suitable for testing.
|
// NewCtx creates and returns a root Context suitable for testing.
|
||||||
func NewCtx() mctx.Context {
|
func NewCtx() context.Context {
|
||||||
ctx := mctx.New()
|
ctx := context.Background()
|
||||||
mlog.From(ctx).SetMaxLevel(mlog.DebugLevel)
|
logger := mlog.NewLogger()
|
||||||
return ctx
|
logger.SetMaxLevel(mlog.DebugLevel)
|
||||||
|
return mlog.Set(ctx, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEnv sets the given environment variable on the given Context, such that it
|
// SetEnv sets the given environment variable on the given Context, such that it
|
||||||
// will be used as if it was a real environment variable when the Run function
|
// will be used as if it was a real environment variable when the Run function
|
||||||
// from this package is called.
|
// from this package is called.
|
||||||
func SetEnv(ctx mctx.Context, key, val string) {
|
func SetEnv(ctx context.Context, key, val string) context.Context {
|
||||||
mctx.GetSetMutableValue(ctx, false, ctxKey(0), func(i interface{}) interface{} {
|
prevEnv, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
||||||
m, _ := i.(map[string]string)
|
env := make([][2]string, len(prevEnv), len(prevEnv)+1)
|
||||||
if m == nil {
|
copy(env, prevEnv)
|
||||||
m = map[string]string{}
|
env = append(env, [2]string{key, val})
|
||||||
}
|
return context.WithValue(ctx, envCtxKey(0), env)
|
||||||
m[key] = val
|
|
||||||
return m
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run performs the following using the given Context:
|
// Run performs the following using the given Context:
|
||||||
@ -45,11 +43,11 @@ func SetEnv(ctx mctx.Context, key, val string) {
|
|||||||
//
|
//
|
||||||
// The intention is that Run is used within a test on a Context created via
|
// The intention is that Run is used within a test on a Context created via
|
||||||
// NewCtx, after any setup functions have been called (e.g. mnet.MListen).
|
// NewCtx, after any setup functions have been called (e.g. mnet.MListen).
|
||||||
func Run(ctx mctx.Context, t *testing.T, body func()) {
|
func Run(ctx context.Context, t *testing.T, body func()) {
|
||||||
envMap, _ := mctx.MutableValue(ctx, ctxKey(0)).(map[string]string)
|
envTups, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
||||||
env := make([]string, 0, len(envMap))
|
env := make([]string, 0, len(envTups))
|
||||||
for key, val := range envMap {
|
for _, tup := range envTups {
|
||||||
env = append(env, key+"="+val)
|
env = append(env, tup[0]+"="+tup[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mcfg.Populate(ctx, mcfg.SourceEnv{Env: env}); err != nil {
|
if err := mcfg.Populate(ctx, mcfg.SourceEnv{Env: env}); err != nil {
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
func TestRun(t *T) {
|
func TestRun(t *T) {
|
||||||
ctx := NewCtx()
|
ctx := NewCtx()
|
||||||
arg := mcfg.RequiredString(ctx, "arg", "Required by this test")
|
ctx, arg := mcfg.RequiredString(ctx, "arg", "Required by this test")
|
||||||
SetEnv(ctx, "ARG", "foo")
|
ctx = SetEnv(ctx, "ARG", "foo")
|
||||||
Run(ctx, t, func() {
|
Run(ctx, t, func() {
|
||||||
if *arg != "foo" {
|
if *arg != "foo" {
|
||||||
t.Fatalf(`arg not set to "foo", is set to %q`, *arg)
|
t.Fatalf(`arg not set to "foo", is set to %q`, *arg)
|
||||||
|
Loading…
Reference in New Issue
Block a user