Config changes
- a source's interval must be greater than 0: a source is turned off with enabled, not with a zero interval, so JSCSourcesSource rejects an interval of 0 or less (its validate is promoted to each source's section); - max_message_size rejects -1: JSCServerWebSocket rejects a max_message_size of -1 (util.ByteSizeNoLimit), as a client could then exhaust the server's memory; - every setting documents what 0 or empty means (zero/none, no limit or a default).
This commit is contained in:
1 parent
49b824d5a2
commit
0b5fde9563
11 files changed
+115
-22
No files matched your search
+16
-11
@@ -33,23 +33,23 @@ is kept.
|
|||||||
| Key | Type | Default | Notes |
|
| Key | Type | Default | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `logging.level` | string | `warn` | One of `debug`, `info`, `warn`, `error` |
|
| `logging.level` | string | `warn` | One of `debug`, `info`, `warn`, `error` |
|
||||||
| `logging.file` | string | | Log file for `fetch service` (unless `--foreground`); other commands log to the terminal |
|
| `logging.file` | string | | Log file for `fetch service` (unless `--foreground`); other commands log to the terminal. Empty: log to the terminal |
|
||||||
| `database.file` | string | | Path to the SQLite database file, used by the `migrate` commands |
|
| `database.file` | string | | Path to the SQLite database file, used by the `migrate` commands. Empty: commands that need the database fail |
|
||||||
| `sources.<source>.enabled` | bool | `false` | For every source |
|
| `sources.<source>.enabled` | bool | `false` | For every source |
|
||||||
| `sources.<source>.interval` | duration | `10m` | For every source; e.g. `30s`, `1h` |
|
| `sources.<source>.interval` | duration | `10m` | For every source; e.g. `30s`, `1h`. Must be greater than 0; turn a source off with `enabled` |
|
||||||
| `sources.find_a_tender.api_root` | string | `https://www.find-tender.service.gov.uk/api/1.0` | URL of the Find a Tender API |
|
| `sources.find_a_tender.api_root` | string | `https://www.find-tender.service.gov.uk/api/1.0` | URL of the Find a Tender API |
|
||||||
| `sources.fts_mock.api_root` | string | | URL of the mock FTS server (see `internal/mock`) |
|
| `sources.fts_mock.api_root` | string | | URL of the mock FTS server (see `internal/mock`); must be set when the source is enabled |
|
||||||
| `sources.sleeper.block_shutdown` | bool | `false` | Test source: if true, its sleep ignores shutdown |
|
| `sources.sleeper.block_shutdown` | bool | `false` | Test source: if true, its sleep ignores shutdown |
|
||||||
| `server.host` | string | `127.0.0.1` | Address to listen on |
|
| `server.host` | string | `127.0.0.1` | Address to listen on; empty: every interface |
|
||||||
| `server.port` | int | `9393` | Port to listen on |
|
| `server.port` | int | `9393` | Port to listen on; `0`: any free port |
|
||||||
| `server.timeouts.shutdown` | duration | `15s` | How long shutdown waits for clients to close |
|
| `server.timeouts.shutdown` | duration | `15s` | How long shutdown waits for clients to close; `0s`: until they have |
|
||||||
| `server.rate_limits.idle_ttl` | duration | `10m` | How long a client's rate limiter is kept after its last request |
|
| `server.rate_limits.idle_ttl` | duration | `10m` | How long a client's rate limiter is kept after its last request; `0s`: for ever |
|
||||||
| `server.websocket.enable_compression` | bool | `false` | permessage-deflate |
|
| `server.websocket.enable_compression` | bool | `false` | permessage-deflate |
|
||||||
| `server.websocket.max_message_size` | byte size | `32KiB` | Largest message read from a client; `0B` for 32KiB |
|
| `server.websocket.max_message_size` | byte size | `32KiB` | Largest message read from a client; `0B` for 32KiB. `-1` (no limit) is rejected, as a client could then exhaust the server's memory |
|
||||||
| `server.websocket.connection_limit.*` | | | A rate limit rule for new connections from each client IP, WebSocket and plain TCP alike; limiters are kept for `server.rate_limits.idle_ttl` |
|
| `server.websocket.connection_limit.*` | | | A rate limit rule for new connections from each client IP, WebSocket and plain TCP alike; limiters are kept for `server.rate_limits.idle_ttl` |
|
||||||
| `server.websocket.message_rate_limit.*` | | | A rate limit rule for messages from each connection |
|
| `server.websocket.message_rate_limit.*` | | | A rate limit rule for messages from each connection |
|
||||||
| `server.websocket.write_timeout` | duration | `10s` | `0s` for none |
|
| `server.websocket.write_timeout` | duration | `10s` | `0s` for none |
|
||||||
| `server.websocket.ping_interval` | duration | `30s` | |
|
| `server.websocket.ping_interval` | duration | `30s` | How long a connection may be idle before it is pinged; `0s`: no pings |
|
||||||
| `server.websocket.pong_timeout` | duration | `10s` | `0s` for none |
|
| `server.websocket.pong_timeout` | duration | `10s` | `0s` for none |
|
||||||
|
|
||||||
The current sources are `find_a_tender`, `fts_mock` and `sleeper`. See
|
The current sources are `find_a_tender`, `fts_mock` and `sleeper`. See
|
||||||
@@ -57,6 +57,8 @@ The current sources are `find_a_tender`, `fts_mock` and `sleeper`. See
|
|||||||
a new source.
|
a new source.
|
||||||
|
|
||||||
A rate limit rule (`JSCRateLimitRule`) has `enabled`, `per_second` and `burst`.
|
A rate limit rule (`JSCRateLimitRule`) has `enabled`, `per_second` and `burst`.
|
||||||
|
It is off unless `enabled` and `per_second` is greater than 0; a `burst` below
|
||||||
|
1 means 1.
|
||||||
It is used under `server.websocket.connection_limit` and
|
It is used under `server.websocket.connection_limit` and
|
||||||
`server.websocket.message_rate_limit`.
|
`server.websocket.message_rate_limit`.
|
||||||
|
|
||||||
@@ -97,8 +99,11 @@ per key:
|
|||||||
- `util.ByteSize` fields work the same way, with size strings such as `"32KiB"`
|
- `util.ByteSize` fields work the same way, with size strings such as `"32KiB"`
|
||||||
or `"1MB"` (KB, MB, ... are powers of 1000; KiB, MiB, ... powers of 1024),
|
or `"1MB"` (KB, MB, ... are powers of 1000; KiB, MiB, ... powers of 1024),
|
||||||
and are given out exactly in the largest unit that divides them (`"1MB"`).
|
and are given out exactly in the largest unit that divides them (`"1MB"`).
|
||||||
|
`-1` means no limit (`util.ByteSizeNoLimit`); no other size may be
|
||||||
|
negative. A setting for which no limit is unsafe rejects `-1`.
|
||||||
- A section can reject values by implementing `validate(key, value) error` on
|
- A section can reject values by implementing `validate(key, value) error` on
|
||||||
its pointer type. `JobSearchConfigLogging` does this for `level`.
|
its pointer type. `JSCLogging` does this for `level`, and
|
||||||
|
`JSCServerWebSocket` for `max_message_size` (rejecting `-1`).
|
||||||
|
|
||||||
Every section implements the `Section` interface defined in `section.go`:
|
Every section implements the `Section` interface defined in `section.go`:
|
||||||
`Map`, `Flatten`, `TypeOf`, `ValueOf` and `SetValue`, each a one-line call into
|
`Map`, `Flatten`, `TypeOf`, `ValueOf` and `SetValue`, each a one-line call into
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import "reflect"
|
|||||||
const JSCKDatabase string = "database"
|
const JSCKDatabase string = "database"
|
||||||
|
|
||||||
// JSCDatabase configures the SQLite database. File is the path to
|
// JSCDatabase configures the SQLite database. File is the path to
|
||||||
// the database file.
|
// the database file; empty means none, and commands that need the database
|
||||||
|
// fail with a message saying so.
|
||||||
type JSCDatabase struct {
|
type JSCDatabase struct {
|
||||||
File string `cfg:"file"`
|
File string `cfg:"file"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import (
|
|||||||
const JSCKLogging string = "logging"
|
const JSCKLogging string = "logging"
|
||||||
|
|
||||||
// JSCLogging configures logging. Level is one of debug, info,
|
// JSCLogging configures logging. Level is one of debug, info,
|
||||||
// warn or error. File, if set, is where commands that log to a file write.
|
// warn or error. File, if set, is where commands that log to a file write;
|
||||||
|
// empty means they log to the terminal.
|
||||||
type JSCLogging struct {
|
type JSCLogging struct {
|
||||||
Level string `cfg:"level" default:"warn"`
|
Level string `cfg:"level" default:"warn"`
|
||||||
File string `cfg:"file"`
|
File string `cfg:"file"`
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package config
|
|||||||
|
|
||||||
import "reflect"
|
import "reflect"
|
||||||
|
|
||||||
// JSCRateLimitRule configures one rate limit. The type is used under several
|
// JSCRateLimitRule configures one rate limit: PerSecond sustained, with
|
||||||
// keys so it has no JSCK* constant and its Flatten keys are not prefixed.
|
// bursts of up to Burst. It is off unless Enabled and PerSecond is greater
|
||||||
|
// than 0; a Burst below 1 means 1. The type is used under several keys so it
|
||||||
|
// has no JSCK* constant and its Flatten keys are not prefixed.
|
||||||
type JSCRateLimitRule struct {
|
type JSCRateLimitRule struct {
|
||||||
Enabled bool `cfg:"enabled" default:"false"`
|
Enabled bool `cfg:"enabled" default:"false"`
|
||||||
PerSecond int `cfg:"per_second" default:"50"`
|
PerSecond int `cfg:"per_second" default:"50"`
|
||||||
Burst int `cfg:"burst" default:"100"`
|
Burst int `cfg:"burst" default:"100"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Section methods (see Section) ----------
|
// ---------- Section methods (see Section) ----------
|
||||||
|
|||||||
@@ -527,3 +527,49 @@ func TestViperValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// max_message_size can't be -1 (no limit), directly or through the whole
|
||||||
|
// configuration; 0 (the default) and explicit sizes are accepted.
|
||||||
|
func TestWebSocketMaxMessageSizeNoLimit(t *testing.T) {
|
||||||
|
var c JSCServerWebSocket
|
||||||
|
if err := c.SetValue("max_message_size", "-1"); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "can't be -1") {
|
||||||
|
t.Errorf("SetValue(max_message_size, -1) = %v; want rejected", err)
|
||||||
|
}
|
||||||
|
if c.MaxMessageSize != 0 {
|
||||||
|
t.Errorf("rejected size was stored: %v", c.MaxMessageSize)
|
||||||
|
}
|
||||||
|
for _, text := range []string{"0B", "64KiB"} {
|
||||||
|
if err := c.SetValue("max_message_size", text); err != nil {
|
||||||
|
t.Errorf("SetValue(max_message_size, %s): %v", text, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf := defaultConfig(t)
|
||||||
|
if err := conf.SetValue("server.websocket.max_message_size", "-1"); err == nil {
|
||||||
|
t.Error("JobSearchConfig.SetValue(server.websocket.max_message_size, -1) succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A source's interval must be greater than 0, through the embedded section's
|
||||||
|
// promoted validate; a source is turned off with enabled.
|
||||||
|
func TestSourceIntervalMustBePositive(t *testing.T) {
|
||||||
|
conf := defaultConfig(t)
|
||||||
|
for _, key := range []string{
|
||||||
|
"sources.find_a_tender.interval",
|
||||||
|
"sources.fts_mock.interval",
|
||||||
|
"sources.sleeper.interval",
|
||||||
|
} {
|
||||||
|
for _, value := range []string{"0s", "-1m"} {
|
||||||
|
err := conf.SetValue(key, value)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "greater than 0") {
|
||||||
|
t.Errorf("SetValue(%s, %s) = %v; want rejected", key, value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := conf.SetValue(key, "1m"); err != nil {
|
||||||
|
t.Errorf("SetValue(%s, 1m): %v", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conf.Sources.FindATender.Interval.Minutes() != 1 {
|
||||||
|
t.Errorf("FindATender.Interval = %v; want 1m", conf.Sources.FindATender.Interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
// JSCKServer is the server section's configuration key.
|
// JSCKServer is the server section's configuration key.
|
||||||
const JSCKServer string = "server"
|
const JSCKServer string = "server"
|
||||||
|
|
||||||
// JSCServer configures the outward server.
|
// JSCServer configures the outward server. Host is the address to listen
|
||||||
|
// on (empty means every interface) and Port the port (0 means any free
|
||||||
|
// port).
|
||||||
type JSCServer struct {
|
type JSCServer struct {
|
||||||
Host string `cfg:"host" default:"127.0.0.1"`
|
Host string `cfg:"host" default:"127.0.0.1"`
|
||||||
Port int `cfg:"port" default:"9393"`
|
Port int `cfg:"port" default:"9393"`
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
const JSCKServerRateLimits string = "rate_limits"
|
const JSCKServerRateLimits string = "rate_limits"
|
||||||
|
|
||||||
// JSCServerRateLimits configures the server's rate limiters. IdleTTL is how
|
// JSCServerRateLimits configures the server's rate limiters. IdleTTL is how
|
||||||
// long a client's limiter is kept after its last request.
|
// long a client's limiter is kept after its last request; 0 or less means
|
||||||
|
// limiters are never evicted.
|
||||||
type JSCServerRateLimits struct {
|
type JSCServerRateLimits struct {
|
||||||
IdleTTL time.Duration `cfg:"idle_ttl" default:"10m"`
|
IdleTTL time.Duration `cfg:"idle_ttl" default:"10m"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
const JSCKServerTimeouts string = "timeouts"
|
const JSCKServerTimeouts string = "timeouts"
|
||||||
|
|
||||||
// JSCServerTimeouts configures the server's timeouts. ShutdownTimeout is how
|
// JSCServerTimeouts configures the server's timeouts. ShutdownTimeout is how
|
||||||
// long shutdown waits for clients to close.
|
// long shutdown waits for clients to close; 0 or less means it waits until
|
||||||
|
// they have.
|
||||||
type JSCServerTimeouts struct {
|
type JSCServerTimeouts struct {
|
||||||
ShutdownTimeout time.Duration `cfg:"shutdown" default:"15s"`
|
ShutdownTimeout time.Duration `cfg:"shutdown" default:"15s"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +12,10 @@ import (
|
|||||||
const JSCKServerWebSocket string = "websocket"
|
const JSCKServerWebSocket string = "websocket"
|
||||||
|
|
||||||
// JSCServerWebSocket configures the server's websocket connections.
|
// JSCServerWebSocket configures the server's websocket connections.
|
||||||
|
// MaxMessageSize is the largest message read from a client; 0 means the
|
||||||
|
// default (32KiB), and -1 (no limit) is rejected (see validate). A
|
||||||
|
// WriteTimeout or PongTimeout of 0 or less means no limit, and a
|
||||||
|
// PingInterval of 0 or less turns pings off.
|
||||||
type JSCServerWebSocket struct {
|
type JSCServerWebSocket struct {
|
||||||
EnableCompression bool `cfg:"enable_compression" default:"false"`
|
EnableCompression bool `cfg:"enable_compression" default:"false"`
|
||||||
MaxMessageSize util.ByteSize `cfg:"max_message_size" default:"32KiB"`
|
MaxMessageSize util.ByteSize `cfg:"max_message_size" default:"32KiB"`
|
||||||
@@ -40,3 +45,17 @@ func (c JSCServerWebSocket) ValueOf(key string) (any, error) {
|
|||||||
func (c *JSCServerWebSocket) SetValue(key string, value any) error {
|
func (c *JSCServerWebSocket) SetValue(key string, value any) error {
|
||||||
return setValue(c, key, value)
|
return setValue(c, key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Section specific methods ----------
|
||||||
|
|
||||||
|
// validate checks values before they are stored (see validator).
|
||||||
|
// max_message_size can't be -1 (no limit): a client could then send a
|
||||||
|
// message of any size, and the server holds each message in memory.
|
||||||
|
func (c *JSCServerWebSocket) validate(key string, value any) error {
|
||||||
|
if key == "max_message_size" && value.(util.ByteSize) == util.ByteSizeNoLimit {
|
||||||
|
return errors.New(
|
||||||
|
"max_message_size can't be -1 (no limit): set an explicit size, or 0 for the default",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import "reflect"
|
|||||||
const JSCKSourceFTSMock string = "fts_mock"
|
const JSCKSourceFTSMock string = "fts_mock"
|
||||||
|
|
||||||
// JSCSourceFTSMock configures fetching from a Find a Tender mock
|
// JSCSourceFTSMock configures fetching from a Find a Tender mock
|
||||||
// server (cmd/mock-fts) at APIRoot.
|
// server (cmd/mock-fts) at APIRoot, which has no default and must be set when
|
||||||
|
// the source is enabled.
|
||||||
type JSCSourceFTSMock struct {
|
type JSCSourceFTSMock struct {
|
||||||
JSCSourcesSource
|
JSCSourcesSource
|
||||||
APIRoot string `cfg:"api_root"`
|
APIRoot string `cfg:"api_root"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -39,7 +40,9 @@ func (c *JSCSources) SetValue(key string, value any) error {
|
|||||||
// ========== Generic source ==========
|
// ========== Generic source ==========
|
||||||
|
|
||||||
// JSCSourcesSource holds the settings every source has, and is embedded
|
// JSCSourcesSource holds the settings every source has, and is embedded
|
||||||
// in each source's section. Interval is how often an enabled source fetches.
|
// in each source's section. Interval is how often an enabled source fetches;
|
||||||
|
// it must be greater than 0 (validate rejects 0 or less: a source is turned
|
||||||
|
// off with Enabled, not with its interval).
|
||||||
type JSCSourcesSource struct {
|
type JSCSourcesSource struct {
|
||||||
Enabled bool `cfg:"enabled"`
|
Enabled bool `cfg:"enabled"`
|
||||||
Interval time.Duration `cfg:"interval" default:"10m"`
|
Interval time.Duration `cfg:"interval" default:"10m"`
|
||||||
@@ -67,3 +70,14 @@ func (c *JSCSourcesSource) SetValue(key string, value any) error {
|
|||||||
|
|
||||||
// Keys returns the keys directly in the section.
|
// Keys returns the keys directly in the section.
|
||||||
func (c *JSCSourcesSource) Keys() []string { return keysOf(c) }
|
func (c *JSCSourcesSource) Keys() []string { return keysOf(c) }
|
||||||
|
|
||||||
|
// validate checks values before they are stored (see validator); it is
|
||||||
|
// promoted to each source's section. interval must be greater than 0.
|
||||||
|
func (c *JSCSourcesSource) validate(key string, value any) error {
|
||||||
|
if key == "interval" && value.(time.Duration) <= 0 {
|
||||||
|
return errors.New(
|
||||||
|
"interval must be greater than 0; set enabled to false to turn the source off",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user