Skip to content

Commit 1262929

Browse files
Add CloseTime to OperationCompletion* structs (#59)
What? -- - Added `CloseTime` to the various OperationCompletion* structs. Why? -- - `CloseTime` is useful in determining an order of events from completion callbacks. We already have `StartTime`, but adding `CloseTime` adds flexibility and is generally useful as for metrics. - In CHASM Scheduler, `CloseTime` is used to determine the earliest possible time our system could kick off a buffered start in sequence of a prior start completing (drives a customer-facing metric). - If we weren't to add this field in Nexus, we'd have to make an additional follow-up `Describe` call to determine it, as part of processing the Nexus callback. --------- Co-authored-by: Roey Berman <roey.berman@gmail.com>
1 parent ce59b2c commit 1262929

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

nexus/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
headerRequestID = "nexus-request-id"
2727
headerLink = "nexus-link"
2828
headerOperationStartTime = "nexus-operation-start-time"
29+
headerOperationCloseTime = "nexus-operation-close-time"
2930
headerRetryable = "nexus-request-retryable"
3031
// HeaderOperationID is the unique ID returned by the StartOperation response for async operations.
3132
// Must be set on callback headers to support completing operations before the start response is received.

nexus/completion.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type OperationCompletionSuccessful struct {
5757
OperationToken string
5858
// StartTime is the time the operation started. Used when a completion callback is received before a started response.
5959
StartTime time.Time
60+
// CloseTime is the time the operation completed. Used when a completion callback is received before a started response.
61+
CloseTime time.Time
6062
// Links are used to link back to the operation when a completion callback is received before a started response.
6163
Links []Link
6264
}
@@ -77,6 +79,8 @@ type OperationCompletionSuccessfulOptions struct {
7779
OperationToken string
7880
// StartTime is the time the operation started. Used when a completion callback is received before a started response.
7981
StartTime time.Time
82+
// CloseTime is the time the operation completed. Used when a completion callback is received before a started response.
83+
CloseTime time.Time
8084
// Links are used to link back to the operation when a completion callback is received before a started response.
8185
Links []Link
8286
}
@@ -117,6 +121,7 @@ func NewOperationCompletionSuccessful(result any, options OperationCompletionSuc
117121
OperationID: options.OperationID,
118122
OperationToken: options.OperationToken,
119123
StartTime: options.StartTime,
124+
CloseTime: options.CloseTime,
120125
Links: options.Links,
121126
}, nil
122127
}
@@ -147,6 +152,9 @@ func (c *OperationCompletionSuccessful) applyToHTTPRequest(request *http.Request
147152
if c.Header.Get(headerOperationStartTime) == "" && !c.StartTime.IsZero() {
148153
request.Header.Set(headerOperationStartTime, c.StartTime.Format(http.TimeFormat))
149154
}
155+
if c.Header.Get(headerOperationCloseTime) == "" && !c.CloseTime.IsZero() {
156+
request.Header.Set(headerOperationCloseTime, c.CloseTime.Format(time.RFC3339Nano))
157+
}
150158
if c.Header.Get(headerLink) == "" {
151159
if err := addLinksToHTTPHeader(c.Links, request.Header); err != nil {
152160
return err
@@ -176,6 +184,8 @@ type OperationCompletionUnsuccessful struct {
176184
OperationToken string
177185
// StartTime is the time the operation started. Used when a completion callback is received before a started response.
178186
StartTime time.Time
187+
// CloseTime is the time the operation completed. This may be different from the time the completion callback is delivered.
188+
CloseTime time.Time
179189
// Links are used to link back to the operation when a completion callback is received before a started response.
180190
Links []Link
181191
// Failure object to send with the completion.
@@ -198,6 +208,8 @@ type OperationCompletionUnsuccessfulOptions struct {
198208
OperationToken string
199209
// StartTime is the time the operation started. Used when a completion callback is received before a started response.
200210
StartTime time.Time
211+
// CloseTime is the time the operation completed. This may be different from the time the completion callback is delivered.
212+
CloseTime time.Time
201213
// Links are used to link back to the operation when a completion callback is received before a started response.
202214
Links []Link
203215
}
@@ -217,6 +229,7 @@ func NewOperationCompletionUnsuccessful(error *OperationError, options Operation
217229
OperationID: options.OperationID,
218230
OperationToken: options.OperationToken,
219231
StartTime: options.StartTime,
232+
CloseTime: options.CloseTime,
220233
Links: options.Links,
221234
}, nil
222235
}
@@ -245,6 +258,9 @@ func (c *OperationCompletionUnsuccessful) applyToHTTPRequest(request *http.Reque
245258
if c.Header.Get(headerOperationStartTime) == "" && !c.StartTime.IsZero() {
246259
request.Header.Set(headerOperationStartTime, c.StartTime.Format(http.TimeFormat))
247260
}
261+
if c.Header.Get(headerOperationCloseTime) == "" && !c.CloseTime.IsZero() {
262+
request.Header.Set(headerOperationCloseTime, c.CloseTime.Format(time.RFC3339Nano))
263+
}
248264
if c.Header.Get(headerLink) == "" {
249265
if err := addLinksToHTTPHeader(c.Links, request.Header); err != nil {
250266
return err
@@ -277,6 +293,8 @@ type CompletionRequest struct {
277293
OperationToken string
278294
// StartTime is the time the operation started. Used when a completion callback is received before a started response.
279295
StartTime time.Time
296+
// CloseTime is the time the operation completed. This may be different from the time the completion callback is delivered.
297+
CloseTime time.Time
280298
// Links are used to link back to the operation when a completion callback is received before a started response.
281299
Links []Link
282300
// Parsed from request and set if State is failed or canceled.
@@ -335,6 +353,13 @@ func (h *completionHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *h
335353
return
336354
}
337355
}
356+
if closeTimeHeader := request.Header.Get(headerOperationCloseTime); closeTimeHeader != "" {
357+
var parseTimeErr error
358+
if completion.CloseTime, parseTimeErr = time.Parse(time.RFC3339Nano, closeTimeHeader); parseTimeErr != nil {
359+
h.writeFailure(writer, HandlerErrorf(HandlerErrorTypeBadRequest, "failed to parse operation close time header"))
360+
return
361+
}
362+
}
338363
var decodeErr error
339364
if completion.Links, decodeErr = getLinksFromHeader(request.Header); decodeErr != nil {
340365
h.writeFailure(writer, HandlerErrorf(HandlerErrorTypeBadRequest, "failed to decode links from request headers"))

nexus/completion_test.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import (
1313
)
1414

1515
type successfulCompletionHandler struct {
16+
expectedStartTime time.Time
17+
expectedCloseTime time.Time
18+
}
19+
20+
// validateExpectedTime returns false if the times are set but aren't equal.
21+
func validateExpectedTime(expected, actual time.Time, resolution time.Duration) bool {
22+
if expected.IsZero() {
23+
return true
24+
}
25+
26+
expected = expected.Truncate(resolution)
27+
actual = actual.Truncate(resolution)
28+
29+
return expected.Equal(actual)
1630
}
1731

1832
func (h *successfulCompletionHandler) CompleteOperation(ctx context.Context, completion *CompletionRequest) error {
@@ -37,6 +51,12 @@ func (h *successfulCompletionHandler) CompleteOperation(ctx context.Context, com
3751
if len(completion.Links) == 0 {
3852
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected Links to be set on CompletionRequest")
3953
}
54+
if !validateExpectedTime(h.expectedStartTime, completion.StartTime, time.Second) {
55+
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected StartTime to be equal")
56+
}
57+
if !validateExpectedTime(h.expectedCloseTime, completion.CloseTime, time.Millisecond) {
58+
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected CloseTime to be equal")
59+
}
4060
var result int
4161
err := completion.Result.Consume(&result)
4262
if err != nil {
@@ -49,12 +69,19 @@ func (h *successfulCompletionHandler) CompleteOperation(ctx context.Context, com
4969
}
5070

5171
func TestSuccessfulCompletion(t *testing.T) {
52-
ctx, callbackURL, teardown := setupForCompletion(t, &successfulCompletionHandler{}, nil, nil)
72+
startTime := time.Now().Add(-time.Hour).UTC()
73+
closeTime := time.Now().UTC()
74+
75+
ctx, callbackURL, teardown := setupForCompletion(t, &successfulCompletionHandler{
76+
expectedStartTime: startTime,
77+
expectedCloseTime: closeTime,
78+
}, nil, nil)
5379
defer teardown()
5480

5581
completion, err := NewOperationCompletionSuccessful(666, OperationCompletionSuccessfulOptions{
5682
OperationToken: "test-operation-token",
57-
StartTime: time.Now(),
83+
StartTime: startTime,
84+
CloseTime: closeTime,
5885
Links: []Link{{
5986
URL: &url.URL{
6087
Scheme: "https",
@@ -113,7 +140,9 @@ func TestSuccessfulCompletion_CustomSerializer(t *testing.T) {
113140
}
114141

115142
type failureExpectingCompletionHandler struct {
116-
errorChecker func(error) error
143+
errorChecker func(error) error
144+
expectedStartTime time.Time
145+
expectedCloseTime time.Time
117146
}
118147

119148
func (h *failureExpectingCompletionHandler) CompleteOperation(ctx context.Context, completion *CompletionRequest) error {
@@ -135,24 +164,36 @@ func (h *failureExpectingCompletionHandler) CompleteOperation(ctx context.Contex
135164
if len(completion.Links) == 0 {
136165
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected Links to be set on CompletionRequest")
137166
}
167+
if !validateExpectedTime(h.expectedStartTime, completion.StartTime, time.Second) {
168+
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected StartTime to be equal")
169+
}
170+
if !validateExpectedTime(h.expectedCloseTime, completion.CloseTime, time.Millisecond) {
171+
return HandlerErrorf(HandlerErrorTypeBadRequest, "expected CloseTime to be equal")
172+
}
138173

139174
return nil
140175
}
141176

142177
func TestFailureCompletion(t *testing.T) {
178+
startTime := time.Now().Add(-time.Hour).UTC()
179+
closeTime := time.Now().UTC()
180+
143181
ctx, callbackURL, teardown := setupForCompletion(t, &failureExpectingCompletionHandler{
144182
errorChecker: func(err error) error {
145183
if err.Error() != "expected message" {
146184
return HandlerErrorf(HandlerErrorTypeBadRequest, "invalid failure: %v", err)
147185
}
148186
return nil
149187
},
188+
expectedStartTime: startTime,
189+
expectedCloseTime: closeTime,
150190
}, nil, nil)
151191
defer teardown()
152192

153193
completion, err := NewOperationCompletionUnsuccessful(NewCanceledOperationError(errors.New("expected message")), OperationCompletionUnsuccessfulOptions{
154194
OperationToken: "test-operation-token",
155-
StartTime: time.Now(),
195+
StartTime: startTime,
196+
CloseTime: closeTime,
156197
Links: []Link{{
157198
URL: &url.URL{
158199
Scheme: "https",
@@ -177,20 +218,26 @@ func TestFailureCompletion(t *testing.T) {
177218

178219
func TestFailureCompletion_CustomFailureConverter(t *testing.T) {
179220
fc := customFailureConverter{}
221+
startTime := time.Now().Add(-time.Hour).UTC()
222+
closeTime := time.Now().UTC()
223+
180224
ctx, callbackURL, teardown := setupForCompletion(t, &failureExpectingCompletionHandler{
181225
errorChecker: func(err error) error {
182226
if !errors.Is(err, errCustom) {
183227
return HandlerErrorf(HandlerErrorTypeBadRequest, "invalid failure, expected a custom error: %v", err)
184228
}
185229
return nil
186230
},
231+
expectedStartTime: startTime,
232+
expectedCloseTime: closeTime,
187233
}, nil, fc)
188234
defer teardown()
189235

190236
completion, err := NewOperationCompletionUnsuccessful(NewCanceledOperationError(errors.New("expected message")), OperationCompletionUnsuccessfulOptions{
191237
FailureConverter: fc,
192238
OperationToken: "test-operation-token",
193-
StartTime: time.Now(),
239+
StartTime: startTime,
240+
CloseTime: closeTime,
194241
Links: []Link{{
195242
URL: &url.URL{
196243
Scheme: "https",

0 commit comments

Comments
 (0)