diff --git a/cayley_test.go b/cayley_test.go index 5544615..f0e85b7 100644 --- a/cayley_test.go +++ b/cayley_test.go @@ -17,6 +17,7 @@ package main import ( "sync" "testing" + "time" "github.com/google/cayley/config" "github.com/google/cayley/db" @@ -294,7 +295,7 @@ var ( cfg = &config.Config{ DatabasePath: "30kmoviedata.nq.gz", DatabaseType: "memstore", - GremlinTimeout: 300, + GremlinTimeout: 300 * time.Second, } ts graph.TripleStore diff --git a/config/config.go b/config/config.go index d521043..04867d8 100644 --- a/config/config.go +++ b/config/config.go @@ -17,29 +17,112 @@ package config import ( "encoding/json" "flag" + "fmt" "os" + "strconv" + "time" "github.com/barakmich/glog" ) type Config struct { + DatabaseType string + DatabasePath string + DatabaseOptions map[string]interface{} + ListenHost string + ListenPort string + ReadOnly bool + GremlinTimeout time.Duration + LoadSize int +} + +type config struct { DatabaseType string `json:"database"` DatabasePath string `json:"db_path"` DatabaseOptions map[string]interface{} `json:"db_options"` ListenHost string `json:"listen_host"` ListenPort string `json:"listen_port"` ReadOnly bool `json:"read_only"` - GremlinTimeout int `json:"gremlin_timeout"` + GremlinTimeout duration `json:"gremlin_timeout"` LoadSize int `json:"load_size"` } -var databasePath = flag.String("dbpath", "/tmp/testdb", "Path to the database.") -var databaseBackend = flag.String("db", "memstore", "Database Backend.") -var host = flag.String("host", "0.0.0.0", "Host to listen on (defaults to all).") -var loadSize = flag.Int("load_size", 10000, "Size of triplesets to load") -var port = flag.String("port", "64210", "Port to listen on.") -var readOnly = flag.Bool("read_only", false, "Disable writing via HTTP.") -var gremlinTimeout = flag.Int("gremlin_timeout", 30, "Number of seconds until an individual query times out.") +func (c *Config) UnmarshalJSON(data []byte) error { + var t config + err := json.Unmarshal(data, &t) + if err != nil { + return err + } + *c = Config{ + DatabaseType: t.DatabaseType, + DatabasePath: t.DatabasePath, + DatabaseOptions: t.DatabaseOptions, + ListenHost: t.ListenHost, + ListenPort: t.ListenPort, + ReadOnly: t.ReadOnly, + GremlinTimeout: time.Duration(t.GremlinTimeout), + LoadSize: t.LoadSize, + } + return nil +} + +func (c *Config) MarshalJSON() ([]byte, error) { + return json.Marshal(config{ + DatabaseType: c.DatabaseType, + DatabasePath: c.DatabasePath, + DatabaseOptions: c.DatabaseOptions, + ListenHost: c.ListenHost, + ListenPort: c.ListenPort, + ReadOnly: c.ReadOnly, + GremlinTimeout: duration(c.GremlinTimeout), + LoadSize: c.LoadSize, + }) +} + +// duration is a time.Duration that satisfies the +// json.UnMarshaler and json.Marshaler interfaces. +type duration time.Duration + +// UnmarshalJSON unmarshals a duration according to the following scheme: +// * If the element is absent the duration is zero. +// * If the element is parsable as a time.Duration, the parsed value is kept. +// * If the element is parsable as a number, that number of seconds is kept. +func (d *duration) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + *d = 0 + return nil + } + text := string(data) + t, err := time.ParseDuration(text) + if err == nil { + *d = duration(t) + return nil + } + i, err := strconv.ParseInt(text, 10, 64) + if err == nil { + *d = duration(time.Duration(i) * time.Second) + return nil + } + // This hack is to get around strconv.ParseFloat + // not handling e-notation for integers. + f, err := strconv.ParseFloat(text, 64) + *d = duration(time.Duration(f) * time.Second) + return err +} + +func (d *duration) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("%q", *d)), nil +} + +var ( + databasePath = flag.String("dbpath", "/tmp/testdb", "Path to the database.") + databaseBackend = flag.String("db", "memstore", "Database Backend.") + host = flag.String("host", "0.0.0.0", "Host to listen on (defaults to all).") + loadSize = flag.Int("load_size", 10000, "Size of triplesets to load") + port = flag.String("port", "64210", "Port to listen on.") + readOnly = flag.Bool("read_only", false, "Disable writing via HTTP.") + gremlinTimeout = flag.Duration("gremlin_timeout", 30*time.Second, "Elapsed time until an individual query times out.") +) func ParseConfigFromFile(filename string) *Config { config := &Config{} diff --git a/docs/Configuration.md b/docs/Configuration.md index 792b476..37b7669 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -74,10 +74,10 @@ All command line flags take precedence over the configuration file. #### **`gremlin_timeout`** - * Type: Integer + * Type: Integer or String * Default: 30 -The value in seconds of the maximum length of time the Javascript runtime should run until cancelling the query and returning a 408 Timeout. A negative value means no limit. +The maximum length of time the Javascript runtime should run until cancelling the query and returning a 408 Timeout. When gremlin_timeout is an integer is is interpretted as seconds, when it is a string it is [parsed](http://golang.org/pkg/time/#ParseDuration) as a Go time.Duration. A negative duration means no limit. ## Per-Database Options diff --git a/query/gremlin/session.go b/query/gremlin/session.go index 3f8bb0b..91c509a 100644 --- a/query/gremlin/session.go +++ b/query/gremlin/session.go @@ -43,15 +43,15 @@ type Session struct { err error script *otto.Script kill chan struct{} - timeoutSec time.Duration + timeout time.Duration emptyEnv *otto.Otto } -func NewSession(ts graph.TripleStore, timeoutSec int, persist bool) *Session { +func NewSession(ts graph.TripleStore, timeout time.Duration, persist bool) *Session { g := Session{ - ts: ts, - limit: -1, - timeoutSec: time.Duration(timeoutSec), + ts: ts, + limit: -1, + timeout: timeout, } g.env = BuildEnviron(&g) if persist { @@ -125,9 +125,9 @@ func (s *Session) runUnsafe(input interface{}) (otto.Value, error) { // Use buffered chan to prevent blocking. s.env.Interrupt = make(chan func(), 1) - if s.timeoutSec >= 0 { + if s.timeout >= 0 { go func() { - time.Sleep(s.timeoutSec * time.Second) + time.Sleep(s.timeout) close(s.kill) s.envLock.Lock() defer s.envLock.Unlock()