-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathreadapi.go
More file actions
360 lines (340 loc) · 12.7 KB
/
Copy pathreadapi.go
File metadata and controls
360 lines (340 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
package flow
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"io"
"slices"
"time"
"unicode/utf8"
"github.com/google/uuid"
"github.com/goware/flow/internal/definition"
"github.com/goware/flow/internal/store"
)
const (
// MaxReadKeys bounds every by-keys batch read before duplicate removal.
MaxReadKeys = store.MaxReadKeys
// DefaultReadPageSize is used when a by-key read filter has PageSize zero.
DefaultReadPageSize = 100
// MaxReadPageSize is the largest public by-key read page.
MaxReadPageSize = 1000
keyedReadCursorVersion = 2
maxReadCursorBytes = 4096
readKindActiveCommands = "active_commands"
readKindHistory = "keyed_history"
)
// ActiveCommand is one queued (or leased) command of a non-terminal run,
// carrying the run's identity. It is the batch, key-addressed read for
// consumers that decorate their own domain rows with dispatch state, without
// touching Flow's tables.
type ActiveCommand struct {
RunID RunID
DefinitionName string
RunKey string
KeyScope KeyScope
RunStatus RunStatus
CommandID CommandID
CommandKey string
CommandName string
Queue string
QueueState QueueState
NextRunAt time.Time
LeaseOwner string
LeaseExpiresAt *time.Time
AttemptOrdinal int
CommandCreatedAt time.Time
}
// ActiveCommandFilter selects bounded queued work for exact run keys. Cursor
// values are opaque and may be reused only with the same Keys filter.
type ActiveCommandFilter struct {
Keys []string
PageSize int
Cursor string
}
// ActiveCommandPage contains one bounded page and an opaque cursor for the next
// page. NextCursor is empty when no later row was observed.
type ActiveCommandPage struct {
Commands []ActiveCommand
NextCursor string
}
// ListActiveCommands returns one bounded page of queued commands for non-terminal
// runs whose run key is in filter.Keys. Rows are ordered by key,
// definition, run creation, run ID, and command ID. An ordinary
// client does not provide a cross-page snapshot; a transaction-scoped client
// uses the caller's transaction and observes its uncommitted writes.
func ListActiveCommands(ctx context.Context, c Client, filter ActiveCommandFilter) (ActiveCommandPage, error) {
client, err := resolveClient(c)
if err != nil {
return ActiveCommandPage{}, err
}
keys, pageSize, cursor, err := prepareKeyedRead(filter.Keys, filter.PageSize, filter.Cursor, readKindActiveCommands)
if err != nil {
return ActiveCommandPage{}, err
}
if len(keys) == 0 {
return ActiveCommandPage{Commands: []ActiveCommand{}}, nil
}
storeFilter := store.ActiveCommandListFilter{Keys: keys, Limit: pageSize + 1}
if cursor != nil {
storeFilter.Cursor = &store.ActiveCommandCursor{
RunKey: cursor.RunKey, DefinitionName: cursor.DefinitionName,
RunCreatedAt: cursor.RunCreatedAt,
RunID: uuid.MustParse(cursor.RunID),
CommandID: uuid.MustParse(cursor.CommandID),
}
}
rows, err := client.runtime.store.ListActiveCommandsInTx(ctx, client.tx, storeFilter)
if err != nil {
return ActiveCommandPage{}, err
}
page := ActiveCommandPage{Commands: make([]ActiveCommand, min(len(rows), pageSize))}
for index := range page.Commands {
page.Commands[index], err = activeCommandFromStore(rows[index])
if err != nil {
return ActiveCommandPage{}, err
}
}
if len(rows) > pageSize {
last := rows[pageSize-1]
page.NextCursor, err = encodeKeyedReadCursor(keyedReadCursor{
Version: keyedReadCursorVersion, Kind: readKindActiveCommands,
KeysHash: keyedReadKeysHash(keys), RunKey: last.RunKey,
DefinitionName: last.DefinitionName, RunCreatedAt: last.RunCreatedAt.UTC(),
RunID: last.RunID.String(), CommandID: last.CommandID.String(),
})
if err != nil {
return ActiveCommandPage{}, err
}
}
return page, nil
}
// KeyedHistoryEntry is a history entry carrying its run's identity.
type KeyedHistoryEntry struct {
DefinitionName string
RunKey string
KeyScope KeyScope
HistoryEntry
}
// KeyedHistoryFilter selects bounded retained history for exact run
// keys. Cursor values are opaque and may be reused only with the same Keys
// filter.
type KeyedHistoryFilter struct {
Keys []string
PageSize int
Cursor string
}
// KeyedHistoryPage contains one bounded page and an opaque cursor for the next
// page. NextCursor is empty when no later row was observed.
type KeyedHistoryPage struct {
Entries []KeyedHistoryEntry
NextCursor string
}
// ListHistoryByRunKeys returns one bounded retained-history page for every
// run that ever held one of filter.Keys. Rows are ordered by key,
// definition, run creation, run ID, and journal position. Journal
// order is preserved within each run. Transaction-scoped clients use the
// caller's transaction and observe their uncommitted writes.
func ListHistoryByRunKeys(ctx context.Context, c Client, filter KeyedHistoryFilter) (KeyedHistoryPage, error) {
client, err := resolveClient(c)
if err != nil {
return KeyedHistoryPage{}, err
}
keys, pageSize, cursor, err := prepareKeyedRead(filter.Keys, filter.PageSize, filter.Cursor, readKindHistory)
if err != nil {
return KeyedHistoryPage{}, err
}
if len(keys) == 0 {
return KeyedHistoryPage{Entries: []KeyedHistoryEntry{}}, nil
}
storeFilter := store.KeyedHistoryListFilter{Keys: keys, Limit: pageSize + 1}
if cursor != nil {
storeFilter.Cursor = &store.KeyedHistoryCursor{
RunKey: cursor.RunKey, DefinitionName: cursor.DefinitionName,
RunCreatedAt: cursor.RunCreatedAt,
RunID: uuid.MustParse(cursor.RunID),
Position: cursor.Position,
}
}
rows, err := client.runtime.store.ListJournalByKeysInTx(ctx, client.tx, storeFilter)
if err != nil {
return KeyedHistoryPage{}, err
}
page := KeyedHistoryPage{Entries: make([]KeyedHistoryEntry, min(len(rows), pageSize))}
for index := range page.Entries {
page.Entries[index], err = keyedHistoryFromStore(rows[index])
if err != nil {
return KeyedHistoryPage{}, err
}
}
if len(rows) > pageSize {
last := rows[pageSize-1]
page.NextCursor, err = encodeKeyedReadCursor(keyedReadCursor{
Version: keyedReadCursorVersion, Kind: readKindHistory,
KeysHash: keyedReadKeysHash(keys), RunKey: last.RunKey,
DefinitionName: last.DefinitionName, RunCreatedAt: last.RunCreatedAt.UTC(),
RunID: last.Entry.RunID.String(), Position: last.Entry.Position,
})
if err != nil {
return KeyedHistoryPage{}, err
}
}
return page, nil
}
func activeCommandFromStore(row store.ActiveCommandRow) (ActiveCommand, error) {
keyScope, err := keyScopeFromString(row.KeyScope)
if err != nil {
return ActiveCommand{}, newError(ErrInvalidState, "decode", "key scope", row.KeyScope, "stored key scope is unknown")
}
runStatus, err := runStatusFromString(row.RunStatus)
if err != nil {
return ActiveCommand{}, newError(ErrInvalidState, "decode", "run status", row.RunStatus, "stored status is unknown")
}
queueState, err := queueStateFromString(row.QueueState)
if err != nil {
return ActiveCommand{}, newError(ErrInvalidState, "decode", "queue state", row.QueueState, "stored state is unknown")
}
work := ActiveCommand{
RunID: RunID(row.RunID.String()), DefinitionName: row.DefinitionName,
RunKey: row.RunKey, KeyScope: keyScope, RunStatus: runStatus,
CommandID: CommandID(row.CommandID.String()), CommandKey: row.CommandKey,
CommandName: row.CommandName, Queue: row.Queue, QueueState: queueState,
NextRunAt: row.NextRunAt, AttemptOrdinal: row.AttemptOrdinal,
CommandCreatedAt: row.CommandCreatedAt,
}
if row.LeaseOwner != nil {
work.LeaseOwner = *row.LeaseOwner
}
if row.LeaseExpiresAt != nil {
expires := *row.LeaseExpiresAt
work.LeaseExpiresAt = &expires
}
return work, nil
}
func keyedHistoryFromStore(row store.KeyedJournalRow) (KeyedHistoryEntry, error) {
keyScope, err := keyScopeFromString(row.KeyScope)
if err != nil {
return KeyedHistoryEntry{}, newError(ErrInvalidState, "decode", "key scope", row.KeyScope, "stored key scope is unknown")
}
history, err := historyEntries([]store.JournalRow{row.Entry})
if err != nil {
return KeyedHistoryEntry{}, err
}
return KeyedHistoryEntry{
DefinitionName: row.DefinitionName, RunKey: row.RunKey,
KeyScope: keyScope, HistoryEntry: history[0],
}, nil
}
type keyedReadCursor struct {
Version int `json:"v"`
Kind string `json:"kind"`
KeysHash string `json:"keys_hash"`
RunKey string `json:"run_key"`
DefinitionName string `json:"definition_name"`
RunCreatedAt time.Time `json:"run_created_at"`
RunID string `json:"run_id"`
CommandID string `json:"command_id,omitempty"`
Position int64 `json:"position,omitempty"`
}
func prepareKeyedRead(keys []string, pageSize int, encodedCursor, kind string) ([]string, int, *keyedReadCursor, error) {
if len(keys) > MaxReadKeys {
return nil, 0, nil, newError(ErrInvalid, "list", "run keys", "", "too many keys")
}
normalized := append([]string(nil), keys...)
for _, key := range normalized {
if key == "" || len(key) > maxRunKeyBytes || !utf8.ValidString(key) {
return nil, 0, nil, newError(ErrInvalid, "list", "run key", "", "key is empty, malformed, or too long")
}
}
slices.Sort(normalized)
normalized = slices.Compact(normalized)
if pageSize == 0 {
pageSize = DefaultReadPageSize
}
if pageSize < 1 || pageSize > MaxReadPageSize {
return nil, 0, nil, newError(ErrInvalid, "list", "page size", "", "page size must be between 1 and 1000")
}
if len(normalized) == 0 {
if encodedCursor != "" {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "cursor requires run keys")
}
return normalized, pageSize, nil, nil
}
if encodedCursor == "" {
return normalized, pageSize, nil, nil
}
cursor, err := decodeKeyedReadCursor(encodedCursor)
if err != nil {
return nil, 0, nil, err
}
if cursor.Version != keyedReadCursorVersion || cursor.Kind != kind || cursor.KeysHash != keyedReadKeysHash(normalized) {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "cursor does not match this read filter")
}
if cursor.RunKey == "" || len(cursor.RunKey) > maxRunKeyBytes || !utf8.ValidString(cursor.RunKey) ||
definition.ValidateName(cursor.DefinitionName) != nil || cursor.RunCreatedAt.IsZero() {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "cursor ordering values are invalid")
}
if _, err := uuid.Parse(cursor.RunID); err != nil {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "cursor run ID is invalid")
}
switch kind {
case readKindActiveCommands:
if cursor.Position != 0 {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "active-command cursor has history state")
}
if _, err := uuid.Parse(cursor.CommandID); err != nil {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "cursor command ID is invalid")
}
case readKindHistory:
if cursor.CommandID != "" || cursor.Position < 1 {
return nil, 0, nil, newError(ErrInvalid, "list", "cursor", "", "history cursor ordering values are invalid")
}
default:
return nil, 0, nil, newError(ErrInvalidState, "list", "cursor", "", "read kind is unknown")
}
return normalized, pageSize, &cursor, nil
}
func keyedReadKeysHash(keys []string) string {
hash := sha256.New()
_, _ = hash.Write([]byte{keyedReadCursorVersion})
var length [4]byte
for _, key := range keys {
binary.BigEndian.PutUint32(length[:], uint32(len(key)))
_, _ = hash.Write(length[:])
_, _ = hash.Write([]byte(key))
}
return hex.EncodeToString(hash.Sum(nil))
}
func encodeKeyedReadCursor(cursor keyedReadCursor) (string, error) {
encoded, err := json.Marshal(cursor)
if err != nil {
return "", newError(ErrInvalidState, "encode", "cursor", "", "cursor cannot be encoded")
}
value := base64.RawURLEncoding.EncodeToString(encoded)
if len(value) > maxReadCursorBytes {
return "", newError(ErrInvalidState, "encode", "cursor", "", "cursor exceeds its internal bound")
}
return value, nil
}
func decodeKeyedReadCursor(value string) (keyedReadCursor, error) {
if len(value) > maxReadCursorBytes {
return keyedReadCursor{}, newError(ErrInvalid, "list", "cursor", "", "cursor is too large")
}
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err != nil || !utf8.Valid(decoded) {
return keyedReadCursor{}, newError(ErrInvalid, "list", "cursor", "", "cursor is malformed")
}
decoder := json.NewDecoder(bytes.NewReader(decoded))
decoder.DisallowUnknownFields()
var cursor keyedReadCursor
if err := decoder.Decode(&cursor); err != nil {
return keyedReadCursor{}, newError(ErrInvalid, "list", "cursor", "", "cursor is malformed")
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return keyedReadCursor{}, newError(ErrInvalid, "list", "cursor", "", "cursor has trailing data")
}
return cursor, nil
}