Skip to content

Commit 7f51a77

Browse files
authored
Merge pull request #964 from tidepool-org/darin/dexcom-slowdown
- Refactor task queue to resolve task-related failures - Remove priority and expiration time from task as unused - Add task timeout watchdog - Add name to queue for debugging purposes - Add revision condition to task Client and Repository API - Set default available time for a new task to now - Update runner deadline to return duration, not time - Add Prometheus metrics to Dexcom client - Add Prometheus metrics to task queue - Add serialization mutex to test logger - Require OAuth client to specify configured http client - Capture response metrics on all outgoing OAuth client requests - Add Prometheus helpers for outgoing clients - Add common CloseCursor function to properly close Mongo cursors and log errors - Add and update tests - Fix typos - https://tidepool.atlassian.net/browse/BACK-4523 - https://tidepool.atlassian.net/browse/BACK-4553
2 parents ab5c6a5 + bc4feb2 commit 7f51a77

105 files changed

Lines changed: 5514 additions & 2172 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ before_install:
2929
- sudo apt-get install --allow-downgrades -y docker-buildx-plugin mongodb-org=${MONGODB} mongodb-org-database=${MONGODB} mongodb-org-server=${MONGODB} mongodb-mongosh=${MONGOSH} mongodb-org-mongos=${MONGODB} mongodb-org-tools
3030
- mkdir -p /var/ramfs/mongodb/data
3131
- /usr/bin/mongod --dbpath /var/ramfs/mongodb/data --bind_ip 127.0.0.1 --replSet rs0 --logpath /var/ramfs/mongodb/mongod.log &> /dev/null &
32-
- until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done
32+
- until nc -z 127.0.0.1 27017; do echo Waiting for MongoDB; sleep 1; done
3333
- /usr/bin/mongosh --eval 'rs.initiate(); while (rs.status().startupStatus || (rs.status().hasOwnProperty("myState") && rs.status().myState != 1)) { printjson( rs.status() ); sleep(1000); }; printjson( rs.status() );'
3434
- echo -e "machine github.com\n login $GITHUB_TOKEN" > ~/.netrc
3535

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ vet: tmp
240240
@echo "go vet ./..."
241241
@cd $(ROOT_DIRECTORY) && \
242242
{ [ -z `go env GOWORK` ] || GOWORK_FLAGS=-mod=readonly; } && \
243-
go vet $${GOWORK_FLAGS:-} ./... > _tmp/govet.out 2>&1 || \
243+
$(TIMING_CMD) go vet $${GOWORK_FLAGS:-} ./... > _tmp/govet.out 2>&1 || \
244244
(diff .govetignore _tmp/govet.out && exit 1)
245245

246246
vet-ignore:

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,47 @@ Review all pending changes to all dependencies. If any changes could have a nega
159159
Ensure the `ci-build` and `ci-test` Makefile targets pass using the target Golang version.
160160

161161
If you previously noted any changes or issues of concern, perform any explicit tests necessary.
162+
163+
## Prometheus Metrics
164+
165+
See source files for further details about and usage of each metric.
166+
167+
### Summary Store
168+
169+
* `tidepool_summary_queue_lag` - (histogram) - the current summary queue lag, in minutes
170+
* `tidepool_summary_queue_length` - (gauge) - the current summary queue length, in number of summaries
171+
172+
### C2C
173+
174+
#### Abbott
175+
176+
* `tidepool_abbott_api_request_count` - (counter) - Abbott API request count, sorted by method, path, and status
177+
* `tidepool_abbott_api_request_duration_seconds` - (histogram) - Abbott API duration of each request, in seconds, sorted by method, path, and status
178+
179+
#### Dexcom
180+
181+
* `tidepool_dexcom_api_request_count` - (counter) - Dexcom API request count, sorted by method, path, and status
182+
* `tidepool_dexcom_api_request_duration_seconds` - (histogram) - Dexcom API duration of each request, in seconds, sorted by method, path, and status
183+
* `tidepool_dexcom_api_request_time_seconds` - (histogram) - Dexcom API duration of each request, as reported in the "request-time" response header from Dexcom, in seconds, sorted by method, path, and status
184+
185+
#### Oura
186+
187+
* `tidepool_oura_api_request_count` - (counter) - Oura API request count, sorted by method, path, and status
188+
* `tidepool_oura_api_request_duration_seconds` - (histogram) - Oura API duration of each request, in seconds, sorted by method, path, and status
189+
190+
### Task
191+
192+
#### Queue
193+
194+
* `tidepool_task_workers_total` - (gauge) - configured number of task queue workers, sorted by queue (per config)
195+
* `tidepool_task_workers_available` - (gauge) - number of available task queue workers, sorted by queue
196+
* `tidepool_task_runner_not_found_total` - (counter) - total number of task runs with no registered runner for the task type, sorted by type (ideally zero)
197+
* `tidepool_task_run_duration_seconds` - (histogram) - duration of task runs in seconds, sorted by type
198+
* `tidepool_task_runner_timeout_exceeded_total` - (counter) - total number of task runs that exceeded the runner timeout, sorted by type and disposition ("blocked", "recovered") (ideally zero)
199+
* `tidepool_task_run_panic_total` - (counter) - total number of task runs that panicked, sorted by type (ideally 0)
200+
201+
#### Store
202+
203+
* `tidepool_task_type_state_total` - (counter) - total number of tasks run, sorted by type and state
204+
* `tidepool_task_type_lost_completion_total` - (counter) - total number of task completions dropped because the claim-token compare-and-swap missed, sorted by type (ideally low-ish)
205+
* `tidepool_task_type_revision_mismatch_total` - (counter) - total number of task revisions that do not match the task revision in the database, sorted by type (ideally zero)

auth/client/external.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package client
33
import (
44
"context"
55
"net/http"
6-
"strconv"
76
"sync"
87
"time"
98

@@ -16,6 +15,7 @@ import (
1615
"github.com/tidepool-org/platform/auth"
1716
"github.com/tidepool-org/platform/client"
1817
"github.com/tidepool-org/platform/config"
18+
"github.com/tidepool-org/platform/duration"
1919
"github.com/tidepool-org/platform/errors"
2020
"github.com/tidepool-org/platform/log"
2121
"github.com/tidepool-org/platform/permission"
@@ -380,13 +380,10 @@ func (l *externalConfigReporterLoader) Load(cfg *ExternalConfig) error {
380380
return err
381381
}
382382
cfg.ServerSessionTokenSecret = l.Reporter.GetWithDefault("server_session_token_secret", "")
383-
if serverSessionTokenTimeoutString, err := l.Reporter.Get("server_session_token_timeout"); err == nil {
384-
var serverSessionTokenTimeoutInteger int64
385-
serverSessionTokenTimeoutInteger, err = strconv.ParseInt(serverSessionTokenTimeoutString, 10, 0)
386-
if err != nil {
387-
return errors.New("server session token timeout is invalid")
388-
}
389-
cfg.ServerSessionTokenTimeout = time.Duration(serverSessionTokenTimeoutInteger) * time.Second
383+
if serverSessionTokenTimeout, err := duration.Parse(l.Reporter.GetWithDefault("server_session_token_timeout", cfg.ServerSessionTokenTimeout.String()), time.Second); err != nil {
384+
return errors.New("server session token timeout is invalid")
385+
} else {
386+
cfg.ServerSessionTokenTimeout = serverSessionTokenTimeout
390387
}
391388

392389
return nil

auth/service/api/v1/metrics.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package v1
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/ant0ine/go-json-rest/rest"
7+
"github.com/prometheus/client_golang/prometheus"
8+
"github.com/prometheus/client_golang/prometheus/promhttp"
9+
)
10+
11+
func (r *Router) MetricsRoutes() []*rest.Route {
12+
return []*rest.Route{
13+
rest.Get("/v1/metrics", r.PrometheusMetrics),
14+
}
15+
}
16+
17+
func (r *Router) PrometheusMetrics(res rest.ResponseWriter, req *rest.Request) {
18+
// The default go-json-rest middleware gzips the content
19+
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{DisableCompression: true}).
20+
ServeHTTP(res.(http.ResponseWriter), req.Request)
21+
}

auth/service/api/v1/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func NewRouter(svc service.Service) (*Router, error) {
2323

2424
func (r *Router) Routes() []*rest.Route {
2525
routes := [][]*rest.Route{
26+
r.MetricsRoutes(),
2627
r.OAuthRoutes(),
2728
r.ProviderSessionsRoutes(),
2829
r.RestrictedTokensRoutes(),

auth/service/service/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func (c *Client) CreateProviderSession(ctx context.Context, create *auth.Provide
8888

8989
if err = prvdr.OnCreate(ctx, providerSession); err != nil {
9090
log.LoggerFromContext(ctx).WithError(err).Error("Unable to finalize creation of provider session")
91-
if err := c.deleteProviderSession(ctx, repository, providerSession); err != nil {
92-
log.LoggerFromContext(ctx).WithError(err).Warn("Unable to delete provider session")
91+
if deleteErr := c.deleteProviderSession(ctx, repository, providerSession); deleteErr != nil {
92+
log.LoggerFromContext(ctx).WithError(deleteErr).Warn("Unable to delete provider session")
9393
}
9494
return nil, err
9595
}

client/client.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/url"
1010
"reflect"
1111
"strings"
12+
"time"
1213
"unicode/utf8"
1314

1415
"github.com/tidepool-org/platform/errors"
@@ -31,8 +32,7 @@ type ErrorResponseParser interface {
3132
}
3233

3334
type Client struct {
34-
address string
35-
userAgent string
35+
config Config
3636
errorResponseParser ErrorResponseParser
3737
}
3838

@@ -48,14 +48,13 @@ func NewWithErrorParser(cfg *Config, errorResponseParser ErrorResponseParser) (*
4848
}
4949

5050
return &Client{
51-
address: cfg.Address,
52-
userAgent: cfg.UserAgent,
51+
config: *cfg,
5352
errorResponseParser: errorResponseParser,
5453
}, nil
5554
}
5655

5756
func (c *Client) ConstructURL(paths ...string) string {
58-
return ConstructURL(c.address, paths...)
57+
return ConstructURL(c.config.Address, paths...)
5958
}
6059

6160
func (c *Client) AppendURLQuery(urlString string, query map[string]string) string {
@@ -78,25 +77,54 @@ func (c *Client) AppendURLQuery(urlString string, query map[string]string) strin
7877
}
7978

8079
func (c *Client) RequestStreamWithHTTPClient(ctx context.Context, method string, url string, mutators []request.RequestMutator, requestBody interface{}, inspectors []request.ResponseInspector, httpClient *http.Client) (io.ReadCloser, error) {
80+
if ctx == nil {
81+
return nil, errors.New("context is missing")
82+
}
8183
if httpClient == nil {
8284
return nil, errors.New("http client is missing")
8385
}
8486

87+
// The request must carry a cancelable context for the timeout to reach the transport. A deadline on that context
88+
// cannot be used, though, as it would also abort any read of the returned response body, so instead cancel via a
89+
// timer that is stopped once the response headers arrive. The cause preserves the deadline exceeded error.
90+
ctx, cancel := context.WithCancelCause(ctx)
91+
8592
req, err := c.createRequest(ctx, method, url, mutators, requestBody)
8693
if err != nil {
94+
cancel(nil)
8795
return nil, err
8896
}
8997

98+
var timer *time.Timer
99+
if c.config.Timeout > 0 {
100+
timer = time.AfterFunc(c.config.Timeout, func() { cancel(context.DeadlineExceeded) })
101+
}
102+
90103
res, err := httpClient.Do(req)
104+
105+
if timer != nil {
106+
timer.Stop()
107+
}
108+
91109
if err != nil {
110+
cancel(nil)
92111
return nil, errors.Wrapf(err, "unable to perform request to %s %s", method, url)
93112
}
94113

95114
for _, inspector := range inspectors {
96115
inspector.InspectResponse(res)
97116
}
98117

99-
return c.handleResponse(ctx, res, req)
118+
body, err := c.handleResponse(ctx, res, req)
119+
if body == nil {
120+
cancel(nil)
121+
return nil, err
122+
}
123+
124+
return &ReadCloserWithCancelCause{
125+
ReadCloser: body,
126+
CancelCauseFunc: cancel,
127+
}, err
100128
}
101129

102130
func (c *Client) RequestDataWithHTTPClient(ctx context.Context, method string, url string, mutators []request.RequestMutator, requestBody interface{}, responseBody interface{}, inspectors []request.ResponseInspector, httpClient *http.Client) error {
@@ -127,8 +155,8 @@ func (c *Client) createRequest(ctx context.Context, method string, url string, m
127155
return nil, errors.New("url is missing")
128156
}
129157

130-
if c.userAgent != "" {
131-
mutators = append(mutators, request.NewHeaderMutator("User-Agent", c.userAgent))
158+
if c.config.UserAgent != "" {
159+
mutators = append(mutators, request.NewHeaderMutator("User-Agent", c.config.UserAgent))
132160
}
133161

134162
var body io.Reader
@@ -247,6 +275,16 @@ func drainAndClose(reader io.ReadCloser) {
247275
reader.Close()
248276
}
249277

278+
type ReadCloserWithCancelCause struct {
279+
io.ReadCloser
280+
context.CancelCauseFunc
281+
}
282+
283+
func (r *ReadCloserWithCancelCause) Close() error {
284+
defer r.CancelCauseFunc(nil)
285+
return r.ReadCloser.Close()
286+
}
287+
250288
func NewSerializableErrorResponseParser() *SerializableErrorResponseParser {
251289
return &SerializableErrorResponseParser{}
252290
}

client/client_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"strings"
12+
"time"
1213

1314
. "github.com/onsi/ginkgo/v2"
1415
. "github.com/onsi/gomega"
@@ -773,6 +774,40 @@ var _ = Describe("Client", func() {
773774
Expect(server.ReceivedRequests()).To(HaveLen(1))
774775
})
775776
})
777+
778+
Context("with a timeout", func() {
779+
var timeout time.Duration
780+
781+
BeforeEach(func() {
782+
timeout = 100 * time.Millisecond
783+
config.Timeout = timeout
784+
})
785+
786+
It("returns an error if the response is not received within the timeout", func() {
787+
server.AppendHandlers(func(res http.ResponseWriter, req *http.Request) {
788+
time.Sleep(3 * timeout)
789+
})
790+
791+
reader, err = clnt.RequestStreamWithHTTPClient(ctx, method, url, mutators, requestBody, inspectors, httpClient)
792+
Expect(errors.Is(errors.Cause(err), context.DeadlineExceeded)).To(BeTrue())
793+
Expect(reader).To(BeNil())
794+
})
795+
796+
It("does not apply the timeout to reading the response body", func() {
797+
server.AppendHandlers(func(res http.ResponseWriter, req *http.Request) {
798+
res.WriteHeader(http.StatusOK)
799+
res.(http.Flusher).Flush()
800+
time.Sleep(3 * timeout)
801+
res.Write([]byte(responseString))
802+
})
803+
804+
reader, err = clnt.RequestStreamWithHTTPClient(ctx, method, url, mutators, requestBody, inspectors, httpClient)
805+
Expect(err).ToNot(HaveOccurred())
806+
Expect(reader).ToNot(BeNil())
807+
Expect(io.ReadAll(reader)).To(Equal([]byte(responseString)))
808+
Expect(server.ReceivedRequests()).To(HaveLen(1))
809+
})
810+
})
776811
})
777812

778813
Context("RequestDataWithHTTPClient", func() {

client/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package client
22

33
import (
44
"net/url"
5+
"time"
56

67
"github.com/kelseyhightower/envconfig"
78

89
"github.com/tidepool-org/platform/config"
10+
"github.com/tidepool-org/platform/duration"
911
"github.com/tidepool-org/platform/errors"
1012
)
1113

@@ -23,6 +25,9 @@ type Config struct {
2325
//
2426
// More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
2527
UserAgent string `envconfig:"TIDEPOOL_USER_AGENT"`
28+
29+
// Timeout specifies the maximum amount of time a request can take. Zero means no timeout.
30+
Timeout time.Duration `envconfig:"TIDEPOOL_CLIENT_TIMEOUT"`
2631
}
2732

2833
func NewConfig() *Config {
@@ -36,6 +41,11 @@ func (c *Config) Load(loader ConfigLoader) error {
3641
func (c *Config) LoadFromConfigReporter(reporter config.Reporter) error {
3742
c.Address = reporter.GetWithDefault("address", c.Address)
3843
c.UserAgent = reporter.GetWithDefault("user_agent", c.UserAgent)
44+
if timeout, parseErr := duration.Parse(reporter.GetWithDefault("timeout", c.Timeout.String()), time.Second); parseErr != nil {
45+
return errors.New("timeout is invalid")
46+
} else {
47+
c.Timeout = timeout
48+
}
3949
return nil
4050
}
4151

@@ -45,6 +55,9 @@ func (c *Config) Validate() error {
4555
} else if _, err := url.Parse(c.Address); err != nil {
4656
return errors.New("address is invalid")
4757
}
58+
if c.Timeout < 0 {
59+
return errors.New("timeout is invalid")
60+
}
4861

4962
return nil
5063
}
@@ -70,6 +83,11 @@ func NewConfigReporterLoader(reporter config.Reporter) *configReporterLoader {
7083
func (l *configReporterLoader) Load(cfg *Config) error {
7184
cfg.Address = l.Reporter.GetWithDefault("address", cfg.Address)
7285
cfg.UserAgent = l.Reporter.GetWithDefault("user_agent", cfg.UserAgent)
86+
if timeout, parseErr := duration.Parse(l.Reporter.GetWithDefault("timeout", cfg.Timeout.String()), time.Second); parseErr != nil {
87+
return errors.New("timeout is invalid")
88+
} else {
89+
cfg.Timeout = timeout
90+
}
7391
return nil
7492
}
7593

0 commit comments

Comments
 (0)