Repository navigation
Expand file tree
/
Copy pathapp_processes.go
More file actions
354 lines (326 loc) · 11.9 KB
/
Copy pathapp_processes.go
File metadata and controls
354 lines (326 loc) · 11.9 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
package remote
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/Agent-Field/codeaf/internal/session"
)
// These additive doors carry the engine's own process shelf and retained
// commands. They introduce no client-supplied command execution route.
const MethodProcessList = "Process.List"
const MethodProcessRerun = "Process.Rerun"
// Process.Page and Process.Record answer one bounded page of the history and
// one record by number, from the engine that holds the conversation, so a
// window never reads another engine's conversation off the disk while that
// engine is running it.
const MethodProcessPage = "Process.Page"
const MethodProcessRecord = "Process.Record"
// processPageDeadline bounds the engine's walk when the caller's deadline
// does not travel with the call.
const processPageDeadline = 2 * time.Second
type ProcessPageArgs struct {
Limit int `json:"limit"`
Cursor string `json:"cursor,omitempty"`
}
type ProcessRecordArgs struct {
ID int `json:"id"`
}
type processRecordReply struct {
Found bool `json:"found"`
Record session.JobNotice `json:"record"`
}
// ErrProcessPageUnserved is an engine too old to page its history.
var ErrProcessPageUnserved = errors.New("this engine cannot page processes")
func (a *Agent) ProcessPage(ctx context.Context, ask session.ProcessPageAsk) (session.ProcessPage, error) {
// THE CALLER'S DEADLINE TRAVELS WITH THE CALL: a page asked for within two
// seconds gives up at two, never at the wire's own longer ceiling.
raw, err := a.c.call(ctx, MethodProcessPage, ProcessPageArgs{Limit: ask.Limit, Cursor: ask.Cursor})
if err != nil {
if strings.Contains(err.Error(), "no such process action") {
return session.ProcessPage{}, ErrProcessPageUnserved
}
if strings.Contains(err.Error(), session.ErrProcessStale.Error()) {
return session.ProcessPage{}, session.ErrProcessStale
}
return session.ProcessPage{}, err
}
var page session.ProcessPage
err = json.Unmarshal(raw, &page)
return page, err
}
func (a *Agent) ProcessRecord(ctx context.Context, id int) (session.JobNotice, bool, error) {
raw, err := a.c.call(ctx, MethodProcessRecord, ProcessRecordArgs{ID: id})
if err != nil {
if strings.Contains(err.Error(), "no such process action") {
return session.JobNotice{}, false, ErrProcessPageUnserved
}
return session.JobNotice{}, false, err
}
var reply processRecordReply
err = json.Unmarshal(raw, &reply)
return reply.Record, reply.Found, err
}
type ProcessRerunArgs struct {
ID int `json:"id"`
}
func (a *Agent) JobNotices() ([]session.JobNotice, error) {
raw, err := a.c.call(nil, MethodProcessList, nil)
if err != nil {
return nil, err
}
var rows []session.JobNotice
err = json.Unmarshal(raw, &rows)
return rows, err
}
func (a *Agent) RerunProcess(id int) (session.JobNotice, error) {
raw, err := a.c.call(nil, MethodProcessRerun, ProcessRerunArgs{ID: id})
if err != nil {
return session.JobNotice{}, err
}
var row session.JobNotice
err = json.Unmarshal(raw, &row)
return row, err
}
// ── the stamped process actions ────────────────────────────────────────────
// Process.Actions answers what the engine holding a conversation will do with
// one run now; Process.RerunAt and Process.StopAt act on that run only after
// asking the same question again; Process.StopAll stops every job the engine
// runs for the conversation under one shared deadline. Each names its run by
// number AND start, so a window's id never reaches another run of the number.
const (
MethodProcessActions = "Process.Actions"
MethodProcessRerunAt = "Process.RerunAt"
MethodProcessStopAt = "Process.StopAt"
MethodProcessStopAll = "Process.StopAll"
)
// ProcessActArgs names one run: its number and its start in Unix
// milliseconds, zero for a record with no start.
type ProcessActArgs struct {
ID int `json:"id"`
StartedMS int64 `json:"started_ms"`
// Caller deadline survives a queued frame; an expired mutator is never admitted.
DeadlineUnixNano int64 `json:"deadline_unix_nano,omitempty"`
}
// processActReply carries a refusal as a FACT rather than an error, so its
// reason crosses the wire as the word it is and not as a sentence to parse.
type processActReply struct {
NotFound bool `json:"not_found,omitempty"`
Refused string `json:"refused,omitempty"`
Verb string `json:"verb,omitempty"`
Actions *session.ProcessActions `json:"actions,omitempty"`
Record *session.JobNotice `json:"record,omitempty"`
Stop *session.ProcessStopOutcome `json:"stop,omitempty"`
Receipt *session.ProcessStopReceipt `json:"receipt,omitempty"`
}
// ErrProcessActionsUnserved is an engine too old to answer the stamped doors.
var ErrProcessActionsUnserved = errors.New("this engine cannot check a process's actions")
// processActor is the stamped doors, served by an engine's own agent and by a
// remote agent served onward alike.
type processActor interface {
ProcessActions(ctx context.Context, id int, startedMS int64) (session.ProcessActions, error)
RerunProcessAt(ctx context.Context, id int, startedMS int64) (session.JobNotice, error)
StopProcessAt(ctx context.Context, id int, startedMS int64) (session.ProcessStopOutcome, error)
StopConversationProcesses(ctx context.Context) (session.ProcessStopReceipt, error)
}
func (a *Agent) processAct(ctx context.Context, method string, args ProcessActArgs) (processActReply, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return processActReply{}, err
}
if deadline, ok := ctx.Deadline(); ok {
args.DeadlineUnixNano = deadline.UnixNano()
}
raw, err := a.c.call(ctx, method, args)
if err != nil {
if strings.Contains(err.Error(), "no such method") || strings.Contains(err.Error(), "no such process action") {
return processActReply{}, ErrProcessActionsUnserved
}
return processActReply{}, err
}
var reply processActReply
if err := json.Unmarshal(raw, &reply); err != nil {
return processActReply{}, err
}
switch {
case reply.NotFound:
return processActReply{}, session.ErrNoSuchProcess
case reply.Refused != "":
return processActReply{}, &session.ProcessActionRefused{Verb: reply.Verb, Reason: reply.Refused}
}
return reply, nil
}
func (a *Agent) ProcessActions(ctx context.Context, id int, startedMS int64) (session.ProcessActions, error) {
reply, err := a.processAct(ctx, MethodProcessActions, ProcessActArgs{ID: id, StartedMS: startedMS})
if err != nil || reply.Actions == nil {
return session.ProcessActions{}, orMissing(err)
}
return *reply.Actions, nil
}
func (a *Agent) RerunProcessAt(ctx context.Context, id int, startedMS int64) (session.JobNotice, error) {
reply, err := a.processAct(ctx, MethodProcessRerunAt, ProcessActArgs{ID: id, StartedMS: startedMS})
if err != nil || reply.Record == nil {
return session.JobNotice{}, orMissing(err)
}
return *reply.Record, nil
}
func (a *Agent) StopProcessAt(ctx context.Context, id int, startedMS int64) (session.ProcessStopOutcome, error) {
reply, err := a.processAct(ctx, MethodProcessStopAt, ProcessActArgs{ID: id, StartedMS: startedMS})
if err != nil || reply.Stop == nil {
return session.ProcessStopOutcome{}, orMissing(err)
}
return *reply.Stop, nil
}
func (a *Agent) StopConversationProcesses(ctx context.Context) (session.ProcessStopReceipt, error) {
reply, err := a.processAct(ctx, MethodProcessStopAll, ProcessActArgs{})
if err != nil || reply.Receipt == nil {
return session.ProcessStopReceipt{}, orMissing(err)
}
return *reply.Receipt, nil
}
// orMissing is err, or the error of an answer that carried nothing.
func orMissing(err error) error {
if err != nil {
return err
}
return errors.New("the engine answered without the process's facts")
}
// processActDeadline bounds the engine's own work on one stamped door: a
// stop's two graces and a second to see them land, which is the stop-all's
// one shared deadline too.
const processActDeadline = session.ProcessStopAllWait
func serveProcessAct(agent WrappedAgent, call Frame) ([]byte, error) {
door, ok := agent.(processActor)
if !ok {
return nil, errors.New("no such process action")
}
args, aerr := arg[ProcessActArgs](call)
if aerr != nil {
return nil, aerr
}
deadline := time.Now().Add(processActDeadline)
if args.DeadlineUnixNano != 0 {
caller := time.Unix(0, args.DeadlineUnixNano)
if caller.Before(deadline) {
deadline = caller
}
}
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
if err := ctx.Err(); err != nil {
return nil, err
}
var reply processActReply
var err error
if call.Method == MethodProcessStopAll {
receipt, serr := door.StopConversationProcesses(ctx)
reply.Receipt, err = &receipt, serr
} else {
switch call.Method {
case MethodProcessActions:
facts, serr := door.ProcessActions(ctx, args.ID, args.StartedMS)
reply.Actions, err = &facts, serr
case MethodProcessRerunAt:
record, serr := door.RerunProcessAt(ctx, args.ID, args.StartedMS)
reply.Record, err = &record, serr
case MethodProcessStopAt:
stop, serr := door.StopProcessAt(ctx, args.ID, args.StartedMS)
reply.Stop, err = &stop, serr
default:
return nil, errors.New("no such process action")
}
}
var refused *session.ProcessActionRefused
switch {
case errors.Is(err, session.ErrNoSuchProcess):
reply = processActReply{NotFound: true}
case errors.As(err, &refused):
reply = processActReply{Refused: refused.Reason, Verb: refused.Verb}
case err != nil:
return nil, err
}
return json.Marshal(reply)
}
func serveAppProcesses(agent WrappedAgent, call Frame) ([]byte, error) {
switch call.Method {
case MethodProcessActions, MethodProcessRerunAt, MethodProcessStopAt, MethodProcessStopAll:
return serveProcessAct(agent, call)
case MethodProcessList:
door, ok := agent.(interface {
JobNotices() ([]session.JobNotice, error)
})
if !ok {
return nil, errors.New("this engine cannot list processes")
}
rows, err := door.JobNotices()
if err != nil {
return nil, err
}
return json.Marshal(rows)
case MethodProcessRerun:
args, err := arg[ProcessRerunArgs](call)
if err != nil {
return nil, err
}
door, ok := agent.(interface {
RerunProcess(int) (session.JobNotice, error)
})
if !ok {
return nil, errors.New("this engine cannot rerun processes")
}
row, err := door.RerunProcess(args.ID)
if err != nil {
return nil, err
}
return json.Marshal(row)
case MethodProcessPage:
args, err := arg[ProcessPageArgs](call)
if err != nil {
return nil, err
}
door, ok := agent.(interface {
ProcessPage(context.Context, session.ProcessPageAsk) (session.ProcessPage, error)
})
if !ok {
return nil, errors.New("no such process action")
}
ctx, cancel := context.WithTimeout(context.Background(), processPageDeadline)
defer cancel()
page, err := door.ProcessPage(ctx, session.ProcessPageAsk{Limit: args.Limit, Cursor: args.Cursor})
if err != nil {
return nil, err
}
return json.Marshal(page)
case MethodProcessRecord:
args, err := arg[ProcessRecordArgs](call)
if err != nil {
return nil, err
}
// AN ENGINE'S AGENT ANSWERS BY NUMBER; A REMOTE AGENT SERVED ONWARD (a
// relay) answers by number within a context. Both are the same door.
var record session.JobNotice
var found bool
switch door := agent.(type) {
case interface {
ProcessRecord(int) (session.JobNotice, bool, error)
}:
record, found, err = door.ProcessRecord(args.ID)
case interface {
ProcessRecord(context.Context, int) (session.JobNotice, bool, error)
}:
ctx, cancel := context.WithTimeout(context.Background(), processPageDeadline)
record, found, err = door.ProcessRecord(ctx, args.ID)
cancel()
default:
return nil, errors.New("no such process action")
}
if err != nil {
return nil, err
}
return json.Marshal(processRecordReply{Found: found, Record: record})
}
return nil, errors.New("no such process action")
}