@fluidframework/task-manager 2.60.0 → 2.61.0-355516
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mocharc.cjs +1 -2
- package/dist/interfaces.d.ts +3 -5
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/legacy.d.ts +2 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/taskManager.d.ts +21 -7
- package/dist/taskManager.d.ts.map +1 -1
- package/dist/taskManager.js +266 -170
- package/dist/taskManager.js.map +1 -1
- package/lib/interfaces.d.ts +3 -5
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.js.map +1 -1
- package/lib/legacy.d.ts +2 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/taskManager.d.ts +21 -7
- package/lib/taskManager.d.ts.map +1 -1
- package/lib/taskManager.js +253 -157
- package/lib/taskManager.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +38 -25
- package/src/interfaces.ts +3 -5
- package/src/packageVersion.ts +1 -1
- package/src/taskManager.ts +293 -178
package/src/taskManager.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EventEmitter } from "@fluid-internal/client-utils";
|
|
7
|
-
import
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
AttachState,
|
|
9
|
+
type ReadOnlyInfo,
|
|
10
|
+
} from "@fluidframework/container-definitions/internal";
|
|
11
|
+
import { assert } from "@fluidframework/core-utils/internal";
|
|
9
12
|
import type {
|
|
10
13
|
IChannelAttributes,
|
|
11
14
|
IFluidDataStoreRuntime,
|
|
@@ -51,6 +54,18 @@ interface IPendingOp {
|
|
|
51
54
|
messageId: number;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
function assertIsTaskManagerOperation(op: unknown): asserts op is ITaskManagerOperation {
|
|
58
|
+
assert(
|
|
59
|
+
typeof op === "object" &&
|
|
60
|
+
op !== null &&
|
|
61
|
+
"taskId" in op &&
|
|
62
|
+
typeof op.taskId === "string" &&
|
|
63
|
+
"type" in op &&
|
|
64
|
+
(op.type === "volunteer" || op.type === "abandon" || op.type === "complete"),
|
|
65
|
+
"Not a TaskManager operation",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
const snapshotFileName = "header";
|
|
55
70
|
|
|
56
71
|
/**
|
|
@@ -84,23 +99,20 @@ export class TaskManagerClass
|
|
|
84
99
|
private readonly connectionWatcher: EventEmitter = new EventEmitter();
|
|
85
100
|
// completedWatcher emits an event whenever the local client receives a completed op.
|
|
86
101
|
private readonly completedWatcher: EventEmitter = new EventEmitter();
|
|
102
|
+
// rollbackWatcher emits an event whenever a pending op is rolled back.
|
|
103
|
+
private readonly rollbackWatcher: EventEmitter = new EventEmitter();
|
|
87
104
|
|
|
88
|
-
private
|
|
105
|
+
private nextPendingMessageId: number = 0;
|
|
89
106
|
/**
|
|
90
107
|
* Tracks the most recent pending op for a given task
|
|
91
108
|
*/
|
|
92
|
-
private readonly latestPendingOps = new Map<string, IPendingOp>();
|
|
109
|
+
private readonly latestPendingOps = new Map<string, IPendingOp[]>();
|
|
93
110
|
|
|
94
111
|
/**
|
|
95
112
|
* Tracks tasks that are this client is currently subscribed to.
|
|
96
113
|
*/
|
|
97
114
|
private readonly subscribedTasks = new Set<string>();
|
|
98
115
|
|
|
99
|
-
/**
|
|
100
|
-
* Map to track tasks that have pending complete ops.
|
|
101
|
-
*/
|
|
102
|
-
private readonly pendingCompletedTasks = new Map<string, number[]>();
|
|
103
|
-
|
|
104
116
|
/**
|
|
105
117
|
* Returns the clientId. Will return a placeholder if the runtime is detached and not yet assigned a clientId.
|
|
106
118
|
*/
|
|
@@ -131,16 +143,17 @@ export class TaskManagerClass
|
|
|
131
143
|
|
|
132
144
|
this.opWatcher.on(
|
|
133
145
|
"volunteer",
|
|
134
|
-
(taskId: string, clientId: string, local: boolean, messageId: number) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const pendingOp =
|
|
139
|
-
assert(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
(taskId: string, clientId: string, local: boolean, messageId: number | undefined) => {
|
|
147
|
+
if (local) {
|
|
148
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
149
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
150
|
+
const pendingOp = latestPendingOps.shift();
|
|
151
|
+
assert(
|
|
152
|
+
pendingOp !== undefined && pendingOp.messageId === messageId,
|
|
153
|
+
"Unexpected op",
|
|
154
|
+
);
|
|
155
|
+
assert(pendingOp.type === "volunteer", 0x07c /* "Unexpected op type" */);
|
|
156
|
+
if (latestPendingOps.length === 0) {
|
|
144
157
|
this.latestPendingOps.delete(taskId);
|
|
145
158
|
}
|
|
146
159
|
}
|
|
@@ -151,16 +164,20 @@ export class TaskManagerClass
|
|
|
151
164
|
|
|
152
165
|
this.opWatcher.on(
|
|
153
166
|
"abandon",
|
|
154
|
-
(taskId: string, clientId: string, local: boolean, messageId: number) => {
|
|
155
|
-
if (
|
|
156
|
-
const
|
|
157
|
-
assert(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
(taskId: string, clientId: string, local: boolean, messageId: number | undefined) => {
|
|
168
|
+
if (local) {
|
|
169
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
170
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
171
|
+
const pendingOp = latestPendingOps.shift();
|
|
172
|
+
assert(
|
|
173
|
+
pendingOp !== undefined && pendingOp.messageId === messageId,
|
|
174
|
+
"Unexpected op",
|
|
175
|
+
);
|
|
176
|
+
assert(pendingOp.type === "abandon", 0x07e /* "Unexpected op type" */);
|
|
177
|
+
if (latestPendingOps.length === 0) {
|
|
162
178
|
this.latestPendingOps.delete(taskId);
|
|
163
179
|
}
|
|
180
|
+
this.abandonWatcher.emit("abandon", taskId, messageId);
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
this.removeClientFromQueue(taskId, clientId);
|
|
@@ -169,33 +186,24 @@ export class TaskManagerClass
|
|
|
169
186
|
|
|
170
187
|
this.opWatcher.on(
|
|
171
188
|
"complete",
|
|
172
|
-
(taskId: string, clientId: string, local: boolean, messageId: number) => {
|
|
173
|
-
if (
|
|
174
|
-
const
|
|
175
|
-
assert(
|
|
176
|
-
|
|
177
|
-
if (messageId === pendingOp.messageId) {
|
|
178
|
-
assert(pendingOp.type === "complete", 0x401 /* Unexpected op type */);
|
|
179
|
-
// Delete the pending, because we no longer have an outstanding op
|
|
180
|
-
this.latestPendingOps.delete(taskId);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Remove complete op from this.pendingCompletedTasks
|
|
184
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
189
|
+
(taskId: string, clientId: string, local: boolean, messageId: number | undefined) => {
|
|
190
|
+
if (local) {
|
|
191
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
192
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
193
|
+
const pendingOp = latestPendingOps.shift();
|
|
185
194
|
assert(
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
pendingOp !== undefined && pendingOp.messageId === messageId,
|
|
196
|
+
"Unexpected op",
|
|
188
197
|
);
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
assert(pendingOp.type === "complete", 0x401 /* Unexpected op type */);
|
|
199
|
+
if (latestPendingOps.length === 0) {
|
|
200
|
+
this.latestPendingOps.delete(taskId);
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.completedWatcher.emit("completed", taskId);
|
|
197
|
-
this.emit("completed", taskId);
|
|
198
|
-
}
|
|
204
|
+
this.taskQueues.delete(taskId);
|
|
205
|
+
this.completedWatcher.emit("completed", taskId, messageId);
|
|
206
|
+
this.emit("completed", taskId);
|
|
199
207
|
},
|
|
200
208
|
);
|
|
201
209
|
|
|
@@ -228,18 +236,16 @@ export class TaskManagerClass
|
|
|
228
236
|
this.connectionWatcher.on("disconnect", () => {
|
|
229
237
|
assert(this.clientId !== undefined, 0x1d3 /* "Missing client id on disconnect" */);
|
|
230
238
|
|
|
231
|
-
//
|
|
232
|
-
// After reconnect these will get cleaned up by observing the clientLeaves.
|
|
233
|
-
// However we do need to recognize that we lost the lock if we had it. Calls to .queued() and
|
|
234
|
-
// .assigned() are also connection-state-aware to be consistent.
|
|
239
|
+
// Emit "lost" for any tasks we were assigned to.
|
|
235
240
|
for (const [taskId, clientQueue] of this.taskQueues.entries()) {
|
|
236
241
|
if (this.isAttached() && clientQueue[0] === this.clientId) {
|
|
237
242
|
this.emit("lost", taskId);
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
//
|
|
242
|
-
this.
|
|
246
|
+
// Remove this client from all queues to reflect the new state, since being disconnected automatically removes
|
|
247
|
+
// this client from all queues.
|
|
248
|
+
this.removeClientFromAllQueues(this.clientId);
|
|
243
249
|
});
|
|
244
250
|
}
|
|
245
251
|
|
|
@@ -250,10 +256,15 @@ export class TaskManagerClass
|
|
|
250
256
|
};
|
|
251
257
|
const pendingOp: IPendingOp = {
|
|
252
258
|
type: "volunteer",
|
|
253
|
-
messageId:
|
|
259
|
+
messageId: this.nextPendingMessageId++,
|
|
254
260
|
};
|
|
255
261
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
256
|
-
this.latestPendingOps.
|
|
262
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
263
|
+
if (latestPendingOps === undefined) {
|
|
264
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
265
|
+
} else {
|
|
266
|
+
latestPendingOps.push(pendingOp);
|
|
267
|
+
}
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
private submitAbandonOp(taskId: string): void {
|
|
@@ -263,10 +274,15 @@ export class TaskManagerClass
|
|
|
263
274
|
};
|
|
264
275
|
const pendingOp: IPendingOp = {
|
|
265
276
|
type: "abandon",
|
|
266
|
-
messageId:
|
|
277
|
+
messageId: this.nextPendingMessageId++,
|
|
267
278
|
};
|
|
268
279
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
269
|
-
this.latestPendingOps.
|
|
280
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
281
|
+
if (latestPendingOps === undefined) {
|
|
282
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
283
|
+
} else {
|
|
284
|
+
latestPendingOps.push(pendingOp);
|
|
285
|
+
}
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
private submitCompleteOp(taskId: string): void {
|
|
@@ -276,25 +292,26 @@ export class TaskManagerClass
|
|
|
276
292
|
};
|
|
277
293
|
const pendingOp: IPendingOp = {
|
|
278
294
|
type: "complete",
|
|
279
|
-
messageId:
|
|
295
|
+
messageId: this.nextPendingMessageId++,
|
|
280
296
|
};
|
|
281
297
|
|
|
282
|
-
|
|
283
|
-
|
|
298
|
+
this.submitLocalMessage(op, pendingOp.messageId);
|
|
299
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
300
|
+
if (latestPendingOps === undefined) {
|
|
301
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
284
302
|
} else {
|
|
285
|
-
|
|
303
|
+
latestPendingOps.push(pendingOp);
|
|
286
304
|
}
|
|
287
|
-
|
|
288
|
-
this.submitLocalMessage(op, pendingOp.messageId);
|
|
289
|
-
this.latestPendingOps.set(taskId, pendingOp);
|
|
290
305
|
}
|
|
291
306
|
|
|
292
307
|
/**
|
|
293
308
|
* {@inheritDoc ITaskManager.volunteerForTask}
|
|
294
309
|
*/
|
|
295
310
|
public async volunteerForTask(taskId: string): Promise<boolean> {
|
|
296
|
-
// If we have the lock
|
|
297
|
-
|
|
311
|
+
// If we are both queued and assigned, then we have the lock and do not
|
|
312
|
+
// have any pending abandon/complete ops. In this case we can resolve
|
|
313
|
+
// true immediately.
|
|
314
|
+
if (this.queuedOptimistically(taskId) && this.assigned(taskId)) {
|
|
298
315
|
return true;
|
|
299
316
|
}
|
|
300
317
|
|
|
@@ -306,7 +323,7 @@ export class TaskManagerClass
|
|
|
306
323
|
throw error;
|
|
307
324
|
}
|
|
308
325
|
|
|
309
|
-
if (
|
|
326
|
+
if (this.isDetached()) {
|
|
310
327
|
// Simulate auto-ack in detached scenario
|
|
311
328
|
assert(this.clientId !== undefined, 0x472 /* clientId should not be undefined */);
|
|
312
329
|
this.addClientToQueue(taskId, this.clientId);
|
|
@@ -319,62 +336,82 @@ export class TaskManagerClass
|
|
|
319
336
|
|
|
320
337
|
// This promise works even if we already have an outstanding volunteer op.
|
|
321
338
|
const lockAcquireP = new Promise<boolean>((resolve, reject) => {
|
|
339
|
+
// If we don't send an op (meaning the latest pending op is "volunteer"), nextPendingMessageId
|
|
340
|
+
// will be greater than that prior "volunteer" op's messageId. This is OK because
|
|
341
|
+
// we only use it to filter stale abandon/complete, and not when determining if we
|
|
342
|
+
// acquired the lock.
|
|
343
|
+
const nextPendingMessageId = this.nextPendingMessageId;
|
|
344
|
+
const setupListeners = (): void => {
|
|
345
|
+
this.queueWatcher.on("queueChange", checkIfAcquiredLock);
|
|
346
|
+
this.abandonWatcher.on("abandon", checkIfAbandoned);
|
|
347
|
+
this.connectionWatcher.on("disconnect", rejectOnDisconnect);
|
|
348
|
+
this.completedWatcher.on("completed", checkIfCompleted);
|
|
349
|
+
this.rollbackWatcher.on("rollback", checkIfRolledBack);
|
|
350
|
+
};
|
|
351
|
+
const removeListeners = (): void => {
|
|
352
|
+
this.queueWatcher.off("queueChange", checkIfAcquiredLock);
|
|
353
|
+
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
354
|
+
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
355
|
+
this.completedWatcher.off("completed", checkIfCompleted);
|
|
356
|
+
this.rollbackWatcher.off("rollback", checkIfRolledBack);
|
|
357
|
+
};
|
|
358
|
+
|
|
322
359
|
const checkIfAcquiredLock = (eventTaskId: string): void => {
|
|
323
360
|
if (eventTaskId !== taskId) {
|
|
324
361
|
return;
|
|
325
362
|
}
|
|
326
|
-
|
|
327
363
|
// Also check pending ops here because it's possible we are currently in the queue from a previous
|
|
328
364
|
// lock attempt, but have an outstanding abandon AND the outstanding volunteer for this lock attempt.
|
|
329
365
|
// If we reach the head of the queue based on the previous lock attempt, we don't want to resolve.
|
|
330
|
-
if (this.assigned(taskId)
|
|
331
|
-
|
|
332
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
333
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
334
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
366
|
+
if (this.assigned(taskId)) {
|
|
367
|
+
removeListeners();
|
|
335
368
|
resolve(true);
|
|
336
369
|
}
|
|
337
370
|
};
|
|
338
371
|
|
|
339
|
-
const checkIfAbandoned = (eventTaskId: string): void => {
|
|
372
|
+
const checkIfAbandoned = (eventTaskId: string, messageId: number | undefined): void => {
|
|
340
373
|
if (eventTaskId !== taskId) {
|
|
341
374
|
return;
|
|
342
375
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
if (messageId !== undefined && messageId <= nextPendingMessageId) {
|
|
377
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
removeListeners();
|
|
348
381
|
reject(new Error("Abandoned before acquiring task assignment"));
|
|
349
382
|
};
|
|
350
383
|
|
|
351
384
|
const rejectOnDisconnect = (): void => {
|
|
352
|
-
|
|
353
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
354
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
355
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
385
|
+
removeListeners();
|
|
356
386
|
reject(new Error("Disconnected before acquiring task assignment"));
|
|
357
387
|
};
|
|
358
388
|
|
|
359
|
-
const checkIfCompleted = (eventTaskId: string): void => {
|
|
389
|
+
const checkIfCompleted = (eventTaskId: string, messageId: number | undefined): void => {
|
|
360
390
|
if (eventTaskId !== taskId) {
|
|
361
391
|
return;
|
|
362
392
|
}
|
|
393
|
+
if (messageId !== undefined && messageId <= nextPendingMessageId) {
|
|
394
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
removeListeners();
|
|
398
|
+
resolve(false);
|
|
399
|
+
};
|
|
363
400
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
401
|
+
const checkIfRolledBack = (eventTaskId: string): void => {
|
|
402
|
+
if (eventTaskId !== taskId) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
removeListeners();
|
|
368
407
|
resolve(false);
|
|
369
408
|
};
|
|
370
409
|
|
|
371
|
-
|
|
372
|
-
this.abandonWatcher.on("abandon", checkIfAbandoned);
|
|
373
|
-
this.connectionWatcher.on("disconnect", rejectOnDisconnect);
|
|
374
|
-
this.completedWatcher.on("completed", checkIfCompleted);
|
|
410
|
+
setupListeners();
|
|
375
411
|
});
|
|
376
412
|
|
|
377
|
-
if (!this.
|
|
413
|
+
if (!this.queuedOptimistically(taskId)) {
|
|
414
|
+
// Only send the volunteer op if we are not already queued.
|
|
378
415
|
this.submitVolunteerOp(taskId);
|
|
379
416
|
}
|
|
380
417
|
return lockAcquireP;
|
|
@@ -392,46 +429,82 @@ export class TaskManagerClass
|
|
|
392
429
|
throw new Error("Attempted to subscribe with read-only permissions");
|
|
393
430
|
}
|
|
394
431
|
|
|
432
|
+
let volunteerOpMessageId: number | undefined;
|
|
433
|
+
|
|
395
434
|
const submitVolunteerOp = (): void => {
|
|
435
|
+
volunteerOpMessageId = this.nextPendingMessageId;
|
|
396
436
|
this.submitVolunteerOp(taskId);
|
|
397
437
|
};
|
|
398
438
|
|
|
439
|
+
const setupListeners = (): void => {
|
|
440
|
+
this.abandonWatcher.on("abandon", checkIfAbandoned);
|
|
441
|
+
this.connectionWatcher.on("disconnect", disconnectHandler);
|
|
442
|
+
this.completedWatcher.on("completed", checkIfCompleted);
|
|
443
|
+
this.rollbackWatcher.on("rollback", checkIfRolledBack);
|
|
444
|
+
};
|
|
445
|
+
const removeListeners = (): void => {
|
|
446
|
+
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
447
|
+
this.connectionWatcher.off("disconnect", disconnectHandler);
|
|
448
|
+
this.connectionWatcher.off("connect", submitVolunteerOp);
|
|
449
|
+
this.completedWatcher.off("completed", checkIfCompleted);
|
|
450
|
+
this.rollbackWatcher.off("rollback", checkIfRolledBack);
|
|
451
|
+
};
|
|
452
|
+
|
|
399
453
|
const disconnectHandler = (): void => {
|
|
400
454
|
// Wait to be connected again and then re-submit volunteer op
|
|
401
455
|
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
402
456
|
};
|
|
403
457
|
|
|
404
|
-
const checkIfAbandoned = (eventTaskId: string): void => {
|
|
458
|
+
const checkIfAbandoned = (eventTaskId: string, messageId: number | undefined): void => {
|
|
405
459
|
if (eventTaskId !== taskId) {
|
|
406
460
|
return;
|
|
407
461
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
this.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
462
|
+
// abandonWatcher emits twice for a local abandon() call. When initially called it
|
|
463
|
+
// will emit with undefined messageId. It will emit a second time when the op is
|
|
464
|
+
// ack'd and processed, this time with the messageId for the ack.
|
|
465
|
+
// This condition accounts ensures we don't ignore the initial abandon() emit and
|
|
466
|
+
// only ignore emits associated with ack'd abandon ops that were sent prior to the
|
|
467
|
+
// current volunteer attempt.
|
|
468
|
+
if (
|
|
469
|
+
messageId !== undefined &&
|
|
470
|
+
volunteerOpMessageId !== undefined &&
|
|
471
|
+
messageId <= volunteerOpMessageId
|
|
472
|
+
) {
|
|
473
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
removeListeners();
|
|
414
477
|
this.subscribedTasks.delete(taskId);
|
|
415
478
|
};
|
|
416
479
|
|
|
417
|
-
const checkIfCompleted = (eventTaskId: string): void => {
|
|
480
|
+
const checkIfCompleted = (eventTaskId: string, messageId: number | undefined): void => {
|
|
418
481
|
if (eventTaskId !== taskId) {
|
|
419
482
|
return;
|
|
420
483
|
}
|
|
484
|
+
if (
|
|
485
|
+
messageId !== undefined &&
|
|
486
|
+
volunteerOpMessageId !== undefined &&
|
|
487
|
+
messageId <= volunteerOpMessageId
|
|
488
|
+
) {
|
|
489
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
removeListeners();
|
|
493
|
+
this.subscribedTasks.delete(taskId);
|
|
494
|
+
};
|
|
421
495
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
496
|
+
const checkIfRolledBack = (eventTaskId: string): void => {
|
|
497
|
+
if (eventTaskId !== taskId) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
426
500
|
|
|
501
|
+
removeListeners();
|
|
427
502
|
this.subscribedTasks.delete(taskId);
|
|
428
503
|
};
|
|
429
504
|
|
|
430
|
-
|
|
431
|
-
this.connectionWatcher.on("disconnect", disconnectHandler);
|
|
432
|
-
this.completedWatcher.on("completed", checkIfCompleted);
|
|
505
|
+
setupListeners();
|
|
433
506
|
|
|
434
|
-
if (
|
|
507
|
+
if (this.isDetached()) {
|
|
435
508
|
// Simulate auto-ack in detached scenario
|
|
436
509
|
assert(this.clientId !== undefined, 0x473 /* clientId should not be undefined */);
|
|
437
510
|
this.addClientToQueue(taskId, this.clientId);
|
|
@@ -439,22 +512,19 @@ export class TaskManagerClass
|
|
|
439
512
|
// a real clientId. At that point we should re-enter the queue with a real volunteer op (assuming we are
|
|
440
513
|
// connected).
|
|
441
514
|
this.runtime.once("attached", () => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return;
|
|
446
|
-
} else if (this.connected) {
|
|
515
|
+
// We call scrubClientsNotInQuorum() in case our clientId changed during the attach process.
|
|
516
|
+
this.scrubClientsNotInQuorum();
|
|
517
|
+
if (this.connected) {
|
|
447
518
|
submitVolunteerOp();
|
|
448
519
|
} else {
|
|
449
|
-
this.connectionWatcher.once("connect",
|
|
450
|
-
submitVolunteerOp();
|
|
451
|
-
});
|
|
520
|
+
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
452
521
|
}
|
|
453
522
|
});
|
|
454
523
|
} else if (!this.connected) {
|
|
455
524
|
// If we are disconnected (and attached), wait to be connected and submit volunteer op
|
|
456
525
|
disconnectHandler();
|
|
457
|
-
} else if (!this.
|
|
526
|
+
} else if (!this.queuedOptimistically(taskId)) {
|
|
527
|
+
// We don't need to send a second volunteer op if we just sent one.
|
|
458
528
|
submitVolunteerOp();
|
|
459
529
|
}
|
|
460
530
|
this.subscribedTasks.add(taskId);
|
|
@@ -465,14 +535,13 @@ export class TaskManagerClass
|
|
|
465
535
|
*/
|
|
466
536
|
public abandon(taskId: string): void {
|
|
467
537
|
// Always allow abandon if the client is subscribed to allow clients to unsubscribe while disconnected.
|
|
468
|
-
// Otherwise, we should check to make sure the client is
|
|
469
|
-
|
|
470
|
-
if (!this.subscribed(taskId) && !this.queued(taskId)) {
|
|
538
|
+
// Otherwise, we should check to make sure the client is optimistically queued for the task before trying to abandon.
|
|
539
|
+
if (!this.queuedOptimistically(taskId) && !this.subscribed(taskId)) {
|
|
471
540
|
// Nothing to do
|
|
472
541
|
return;
|
|
473
542
|
}
|
|
474
543
|
|
|
475
|
-
if (
|
|
544
|
+
if (this.isDetached()) {
|
|
476
545
|
// Simulate auto-ack in detached scenario
|
|
477
546
|
assert(this.clientId !== undefined, 0x474 /* clientId is undefined */);
|
|
478
547
|
this.removeClientFromQueue(taskId, this.clientId);
|
|
@@ -480,10 +549,7 @@ export class TaskManagerClass
|
|
|
480
549
|
return;
|
|
481
550
|
}
|
|
482
551
|
|
|
483
|
-
|
|
484
|
-
if (this.queued(taskId)) {
|
|
485
|
-
this.submitAbandonOp(taskId);
|
|
486
|
-
}
|
|
552
|
+
this.submitAbandonOp(taskId);
|
|
487
553
|
this.abandonWatcher.emit("abandon", taskId);
|
|
488
554
|
}
|
|
489
555
|
|
|
@@ -496,11 +562,7 @@ export class TaskManagerClass
|
|
|
496
562
|
}
|
|
497
563
|
|
|
498
564
|
const currentAssignee = this.taskQueues.get(taskId)?.[0];
|
|
499
|
-
return
|
|
500
|
-
currentAssignee !== undefined &&
|
|
501
|
-
currentAssignee === this.clientId &&
|
|
502
|
-
!this.latestPendingOps.has(taskId)
|
|
503
|
-
);
|
|
565
|
+
return currentAssignee !== undefined && currentAssignee === this.clientId;
|
|
504
566
|
}
|
|
505
567
|
|
|
506
568
|
/**
|
|
@@ -512,14 +574,7 @@ export class TaskManagerClass
|
|
|
512
574
|
}
|
|
513
575
|
|
|
514
576
|
assert(this.clientId !== undefined, 0x07f /* "clientId undefined" */);
|
|
515
|
-
|
|
516
|
-
const clientQueue = this.taskQueues.get(taskId);
|
|
517
|
-
// If we have no queue for the taskId, then no one has signed up for it.
|
|
518
|
-
return (
|
|
519
|
-
((clientQueue?.includes(this.clientId) ?? false) &&
|
|
520
|
-
!this.latestPendingOps.has(taskId)) ||
|
|
521
|
-
this.latestPendingOps.get(taskId)?.type === "volunteer"
|
|
522
|
-
);
|
|
577
|
+
return this.taskQueues.get(taskId)?.includes(this.clientId) ?? false;
|
|
523
578
|
}
|
|
524
579
|
|
|
525
580
|
/**
|
|
@@ -539,16 +594,17 @@ export class TaskManagerClass
|
|
|
539
594
|
|
|
540
595
|
// If we are detached we will simulate auto-ack for the complete op. Therefore we only need to send the op if
|
|
541
596
|
// we are attached. Additionally, we don't need to check if we are connected while detached.
|
|
542
|
-
if (this.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
597
|
+
if (this.isDetached()) {
|
|
598
|
+
this.taskQueues.delete(taskId);
|
|
599
|
+
this.completedWatcher.emit("completed", taskId);
|
|
600
|
+
this.emit("completed", taskId);
|
|
601
|
+
return;
|
|
547
602
|
}
|
|
548
603
|
|
|
549
|
-
this.
|
|
550
|
-
|
|
551
|
-
|
|
604
|
+
if (!this.connected) {
|
|
605
|
+
throw new Error("Attempted to complete task in disconnected state");
|
|
606
|
+
}
|
|
607
|
+
this.submitCompleteOp(taskId);
|
|
552
608
|
}
|
|
553
609
|
|
|
554
610
|
/**
|
|
@@ -618,12 +674,24 @@ export class TaskManagerClass
|
|
|
618
674
|
this.connectionWatcher.emit("connect");
|
|
619
675
|
}
|
|
620
676
|
|
|
621
|
-
//
|
|
622
677
|
/**
|
|
623
678
|
* Override resubmit core to avoid resubmission on reconnect. On disconnect we accept our removal from the
|
|
624
679
|
* queues, and leave it up to the user to decide whether they want to attempt to re-enter a queue on reconnect.
|
|
680
|
+
* However, we do need to update latestPendingOps to account for the ops we will no longer be processing.
|
|
625
681
|
*/
|
|
626
|
-
protected reSubmitCore(): void {
|
|
682
|
+
protected reSubmitCore(content: unknown, localOpMetadata: number): void {
|
|
683
|
+
assertIsTaskManagerOperation(content);
|
|
684
|
+
const pendingOps = this.latestPendingOps.get(content.taskId);
|
|
685
|
+
assert(pendingOps !== undefined, "No pending ops for task on resubmit attempt");
|
|
686
|
+
const pendingOpIndex = pendingOps.findIndex(
|
|
687
|
+
(op) => op.messageId === localOpMetadata && op.type === content.type,
|
|
688
|
+
);
|
|
689
|
+
assert(pendingOpIndex !== -1, "Could not match pending op on resubmit attempt");
|
|
690
|
+
pendingOps.splice(pendingOpIndex, 1);
|
|
691
|
+
if (pendingOps.length === 0) {
|
|
692
|
+
this.latestPendingOps.delete(content.taskId);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
627
695
|
|
|
628
696
|
/**
|
|
629
697
|
* Process a task manager operation
|
|
@@ -636,12 +704,12 @@ export class TaskManagerClass
|
|
|
636
704
|
protected processCore(
|
|
637
705
|
message: ISequencedDocumentMessage,
|
|
638
706
|
local: boolean,
|
|
639
|
-
localOpMetadata:
|
|
707
|
+
localOpMetadata: number | undefined,
|
|
640
708
|
): void {
|
|
641
709
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
642
710
|
if (message.type === MessageType.Operation) {
|
|
643
711
|
const op = message.contents as ITaskManagerOperation;
|
|
644
|
-
const messageId = localOpMetadata
|
|
712
|
+
const messageId = localOpMetadata;
|
|
645
713
|
|
|
646
714
|
switch (op.type) {
|
|
647
715
|
case "volunteer": {
|
|
@@ -667,12 +735,6 @@ export class TaskManagerClass
|
|
|
667
735
|
}
|
|
668
736
|
|
|
669
737
|
private addClientToQueue(taskId: string, clientId: string): void {
|
|
670
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
671
|
-
if (pendingIds !== undefined && pendingIds.length > 0) {
|
|
672
|
-
// Ignore the volunteer op if we know this task is about to be completed
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
738
|
// Ensure that the clientId exists in the quorum, or it is placeholderClientId (detached scenario)
|
|
677
739
|
if (
|
|
678
740
|
this.runtime.getQuorum().getMembers().has(clientId) ||
|
|
@@ -685,6 +747,13 @@ export class TaskManagerClass
|
|
|
685
747
|
this.taskQueues.set(taskId, clientQueue);
|
|
686
748
|
}
|
|
687
749
|
|
|
750
|
+
if (clientQueue.includes(clientId)) {
|
|
751
|
+
// We shouldn't re-add the client if it's already in the queue.
|
|
752
|
+
// This may be possible in scenarios where a client was added in
|
|
753
|
+
// while detached.
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
688
757
|
const oldLockHolder = clientQueue[0];
|
|
689
758
|
clientQueue.push(clientId);
|
|
690
759
|
const newLockHolder = clientQueue[0];
|
|
@@ -734,7 +803,12 @@ export class TaskManagerClass
|
|
|
734
803
|
for (const clientQueue of this.taskQueues.values()) {
|
|
735
804
|
const clientIdIndex = clientQueue.indexOf(placeholderClientId);
|
|
736
805
|
if (clientIdIndex !== -1) {
|
|
737
|
-
clientQueue
|
|
806
|
+
if (clientQueue.includes(this.runtime.clientId)) {
|
|
807
|
+
// If the real clientId is already in the queue, just remove the placeholder.
|
|
808
|
+
clientQueue.splice(clientIdIndex, 1);
|
|
809
|
+
} else {
|
|
810
|
+
clientQueue[clientIdIndex] = this.runtime.clientId;
|
|
811
|
+
}
|
|
738
812
|
}
|
|
739
813
|
}
|
|
740
814
|
}
|
|
@@ -758,24 +832,65 @@ export class TaskManagerClass
|
|
|
758
832
|
}
|
|
759
833
|
}
|
|
760
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Checks whether this client is currently assigned or in queue to become assigned, while also accounting
|
|
837
|
+
* for the latest pending ops.
|
|
838
|
+
*/
|
|
839
|
+
private queuedOptimistically(taskId: string): boolean {
|
|
840
|
+
if (this.isAttached() && !this.connected) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
assert(this.clientId !== undefined, 0x07f /* "clientId undefined" */);
|
|
845
|
+
|
|
846
|
+
const inQueue = this.taskQueues.get(taskId)?.includes(this.clientId) ?? false;
|
|
847
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
848
|
+
|
|
849
|
+
const latestPendingOp =
|
|
850
|
+
latestPendingOps !== undefined && latestPendingOps.length > 0
|
|
851
|
+
? latestPendingOps[latestPendingOps.length - 1]
|
|
852
|
+
: undefined;
|
|
853
|
+
const isPendingVolunteer = latestPendingOp?.type === "volunteer";
|
|
854
|
+
const isPendingAbandonOrComplete =
|
|
855
|
+
latestPendingOp?.type === "abandon" || latestPendingOp?.type === "complete";
|
|
856
|
+
// We return true if the client is either in queue already or the latest pending op for this task is a volunteer op.
|
|
857
|
+
// But we should always return false if the latest pending op is an abandon or complete op.
|
|
858
|
+
return (inQueue && !isPendingAbandonOrComplete) || isPendingVolunteer;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Returns true if the client is detached.
|
|
863
|
+
* This is distinct from !this.isAttached() because `isAttached()` also checks if `this._isBoundToContext`
|
|
864
|
+
* is true. We use `isDetached()` to determine if we should simulate auto-ack behavior for ops, which is
|
|
865
|
+
* mainly concerned with if we have been assigned a real clientId yet.
|
|
866
|
+
*/
|
|
867
|
+
private isDetached(): boolean {
|
|
868
|
+
return this.runtime.attachState === AttachState.Detached;
|
|
869
|
+
}
|
|
870
|
+
|
|
761
871
|
protected applyStashedOp(content: unknown): void {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
872
|
+
// We don't apply any stashed ops since during the rehydration process. Since we lose any assigned tasks
|
|
873
|
+
// during rehydration we cannot be assigned any tasks. Additionally, without the in-memory state of the
|
|
874
|
+
// previous dds, we also cannot re-volunteer based on a previous subscribeToTask() call. Since we are
|
|
875
|
+
// unable to be assigned to any tasks, there is no reason to process abandon/complete ops either.
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
880
|
+
*/
|
|
881
|
+
protected rollback(content: unknown, localOpMetadata: unknown): void {
|
|
882
|
+
assert(typeof localOpMetadata === "number", "Expect localOpMetadata to be a number");
|
|
883
|
+
assertIsTaskManagerOperation(content);
|
|
884
|
+
const latestPendingOps = this.latestPendingOps.get(content.taskId);
|
|
885
|
+
assert(latestPendingOps !== undefined, "No pending ops when trying to rollback");
|
|
886
|
+
const pendingOpToRollback = latestPendingOps.pop();
|
|
887
|
+
assert(
|
|
888
|
+
pendingOpToRollback !== undefined && pendingOpToRollback.messageId === localOpMetadata,
|
|
889
|
+
"pending op mismatch",
|
|
890
|
+
);
|
|
891
|
+
if (latestPendingOps.length === 0) {
|
|
892
|
+
this.latestPendingOps.delete(content.taskId);
|
|
779
893
|
}
|
|
894
|
+
this.rollbackWatcher.emit("rollback", content.taskId);
|
|
780
895
|
}
|
|
781
896
|
}
|