@fluidframework/task-manager 2.53.1 → 2.61.0-355054
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/CHANGELOG.md +4 -0
- package/api-report/{task-manager.legacy.alpha.api.md → task-manager.legacy.beta.api.md} +6 -6
- package/dist/interfaces.d.ts +6 -11
- 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 +22 -9
- package/dist/taskManager.d.ts.map +1 -1
- package/dist/taskManager.js +267 -172
- package/dist/taskManager.js.map +1 -1
- package/dist/taskManagerFactory.d.ts +2 -4
- package/dist/taskManagerFactory.d.ts.map +1 -1
- package/dist/taskManagerFactory.js +1 -2
- package/dist/taskManagerFactory.js.map +1 -1
- package/internal.d.ts +1 -1
- package/legacy.d.ts +1 -1
- package/lib/interfaces.d.ts +6 -11
- 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 +22 -9
- package/lib/taskManager.d.ts.map +1 -1
- package/lib/taskManager.js +254 -159
- package/lib/taskManager.js.map +1 -1
- package/lib/taskManagerFactory.d.ts +2 -4
- package/lib/taskManagerFactory.d.ts.map +1 -1
- package/lib/taskManagerFactory.js +1 -2
- package/lib/taskManagerFactory.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +39 -26
- package/src/interfaces.ts +6 -11
- package/src/packageVersion.ts +1 -1
- package/src/taskManager.ts +294 -180
- package/src/taskManagerFactory.ts +2 -4
package/lib/taskManager.js
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
import { EventEmitter } from "@fluid-internal/client-utils";
|
|
6
|
-
import {
|
|
6
|
+
import { AttachState, } from "@fluidframework/container-definitions/internal";
|
|
7
|
+
import { assert } from "@fluidframework/core-utils/internal";
|
|
7
8
|
import { MessageType } from "@fluidframework/driver-definitions/internal";
|
|
8
9
|
import { readAndParse } from "@fluidframework/driver-utils/internal";
|
|
9
10
|
import { SharedObject, createSingleBlobSummary, } from "@fluidframework/shared-object-base/internal";
|
|
11
|
+
function assertIsTaskManagerOperation(op) {
|
|
12
|
+
assert(typeof op === "object" &&
|
|
13
|
+
op !== null &&
|
|
14
|
+
"taskId" in op &&
|
|
15
|
+
typeof op.taskId === "string" &&
|
|
16
|
+
"type" in op &&
|
|
17
|
+
(op.type === "volunteer" || op.type === "abandon" || op.type === "complete"), "Not a TaskManager operation");
|
|
18
|
+
}
|
|
10
19
|
const snapshotFileName = "header";
|
|
11
20
|
/**
|
|
12
21
|
* Placeholder clientId for detached scenarios.
|
|
@@ -16,8 +25,7 @@ const placeholderClientId = "placeholder";
|
|
|
16
25
|
* {@inheritDoc ITaskManager}
|
|
17
26
|
*
|
|
18
27
|
* @sealed
|
|
19
|
-
* @legacy
|
|
20
|
-
* @alpha
|
|
28
|
+
* @legacy @beta
|
|
21
29
|
*/
|
|
22
30
|
export class TaskManagerClass extends SharedObject {
|
|
23
31
|
/**
|
|
@@ -56,7 +64,9 @@ export class TaskManagerClass extends SharedObject {
|
|
|
56
64
|
this.connectionWatcher = new EventEmitter();
|
|
57
65
|
// completedWatcher emits an event whenever the local client receives a completed op.
|
|
58
66
|
this.completedWatcher = new EventEmitter();
|
|
59
|
-
|
|
67
|
+
// rollbackWatcher emits an event whenever a pending op is rolled back.
|
|
68
|
+
this.rollbackWatcher = new EventEmitter();
|
|
69
|
+
this.nextPendingMessageId = 0;
|
|
60
70
|
/**
|
|
61
71
|
* Tracks the most recent pending op for a given task
|
|
62
72
|
*/
|
|
@@ -65,60 +75,47 @@ export class TaskManagerClass extends SharedObject {
|
|
|
65
75
|
* Tracks tasks that are this client is currently subscribed to.
|
|
66
76
|
*/
|
|
67
77
|
this.subscribedTasks = new Set();
|
|
68
|
-
/**
|
|
69
|
-
* Map to track tasks that have pending complete ops.
|
|
70
|
-
*/
|
|
71
|
-
this.pendingCompletedTasks = new Map();
|
|
72
78
|
this.opWatcher.on("volunteer", (taskId, clientId, local, messageId) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const pendingOp =
|
|
77
|
-
assert(pendingOp !== undefined
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
assert(pendingOp.type === "volunteer", 0x07c /* "Unexpected op type" */);
|
|
81
|
-
// Delete the pending, because we no longer have an outstanding op
|
|
79
|
+
if (local) {
|
|
80
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
81
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
82
|
+
const pendingOp = latestPendingOps.shift();
|
|
83
|
+
assert(pendingOp !== undefined && pendingOp.messageId === messageId, "Unexpected op");
|
|
84
|
+
assert(pendingOp.type === "volunteer", 0x07c /* "Unexpected op type" */);
|
|
85
|
+
if (latestPendingOps.length === 0) {
|
|
82
86
|
this.latestPendingOps.delete(taskId);
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
this.addClientToQueue(taskId, clientId);
|
|
86
90
|
});
|
|
87
91
|
this.opWatcher.on("abandon", (taskId, clientId, local, messageId) => {
|
|
88
|
-
if (
|
|
89
|
-
const
|
|
90
|
-
assert(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
if (local) {
|
|
93
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
94
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
95
|
+
const pendingOp = latestPendingOps.shift();
|
|
96
|
+
assert(pendingOp !== undefined && pendingOp.messageId === messageId, "Unexpected op");
|
|
97
|
+
assert(pendingOp.type === "abandon", 0x07e /* "Unexpected op type" */);
|
|
98
|
+
if (latestPendingOps.length === 0) {
|
|
95
99
|
this.latestPendingOps.delete(taskId);
|
|
96
100
|
}
|
|
101
|
+
this.abandonWatcher.emit("abandon", taskId, messageId);
|
|
97
102
|
}
|
|
98
103
|
this.removeClientFromQueue(taskId, clientId);
|
|
99
104
|
});
|
|
100
105
|
this.opWatcher.on("complete", (taskId, clientId, local, messageId) => {
|
|
101
|
-
if (
|
|
102
|
-
const
|
|
103
|
-
assert(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
if (local) {
|
|
107
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
108
|
+
assert(latestPendingOps !== undefined, "No pending ops for task");
|
|
109
|
+
const pendingOp = latestPendingOps.shift();
|
|
110
|
+
assert(pendingOp !== undefined && pendingOp.messageId === messageId, "Unexpected op");
|
|
111
|
+
assert(pendingOp.type === "complete", 0x401 /* Unexpected op type */);
|
|
112
|
+
if (latestPendingOps.length === 0) {
|
|
108
113
|
this.latestPendingOps.delete(taskId);
|
|
109
114
|
}
|
|
110
|
-
// Remove complete op from this.pendingCompletedTasks
|
|
111
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
112
|
-
assert(pendingIds !== undefined && pendingIds.length > 0, 0x402 /* pendingIds is empty */);
|
|
113
|
-
const removed = pendingIds.shift();
|
|
114
|
-
assert(removed === messageId, 0x403 /* Removed complete op id does not match */);
|
|
115
|
-
}
|
|
116
|
-
// For clients in queue, we need to remove them from the queue and raise the proper events.
|
|
117
|
-
if (!local) {
|
|
118
|
-
this.taskQueues.delete(taskId);
|
|
119
|
-
this.completedWatcher.emit("completed", taskId);
|
|
120
|
-
this.emit("completed", taskId);
|
|
121
115
|
}
|
|
116
|
+
this.taskQueues.delete(taskId);
|
|
117
|
+
this.completedWatcher.emit("completed", taskId, messageId);
|
|
118
|
+
this.emit("completed", taskId);
|
|
122
119
|
});
|
|
123
120
|
runtime.getQuorum().on("removeMember", (clientId) => {
|
|
124
121
|
this.removeClientFromAllQueues(clientId);
|
|
@@ -142,17 +139,15 @@ export class TaskManagerClass extends SharedObject {
|
|
|
142
139
|
});
|
|
143
140
|
this.connectionWatcher.on("disconnect", () => {
|
|
144
141
|
assert(this.clientId !== undefined, 0x1d3 /* "Missing client id on disconnect" */);
|
|
145
|
-
//
|
|
146
|
-
// After reconnect these will get cleaned up by observing the clientLeaves.
|
|
147
|
-
// However we do need to recognize that we lost the lock if we had it. Calls to .queued() and
|
|
148
|
-
// .assigned() are also connection-state-aware to be consistent.
|
|
142
|
+
// Emit "lost" for any tasks we were assigned to.
|
|
149
143
|
for (const [taskId, clientQueue] of this.taskQueues.entries()) {
|
|
150
144
|
if (this.isAttached() && clientQueue[0] === this.clientId) {
|
|
151
145
|
this.emit("lost", taskId);
|
|
152
146
|
}
|
|
153
147
|
}
|
|
154
|
-
//
|
|
155
|
-
this.
|
|
148
|
+
// Remove this client from all queues to reflect the new state, since being disconnected automatically removes
|
|
149
|
+
// this client from all queues.
|
|
150
|
+
this.removeClientFromAllQueues(this.clientId);
|
|
156
151
|
});
|
|
157
152
|
}
|
|
158
153
|
submitVolunteerOp(taskId) {
|
|
@@ -162,10 +157,16 @@ export class TaskManagerClass extends SharedObject {
|
|
|
162
157
|
};
|
|
163
158
|
const pendingOp = {
|
|
164
159
|
type: "volunteer",
|
|
165
|
-
messageId:
|
|
160
|
+
messageId: this.nextPendingMessageId++,
|
|
166
161
|
};
|
|
167
162
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
168
|
-
this.latestPendingOps.
|
|
163
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
164
|
+
if (latestPendingOps === undefined) {
|
|
165
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
latestPendingOps.push(pendingOp);
|
|
169
|
+
}
|
|
169
170
|
}
|
|
170
171
|
submitAbandonOp(taskId) {
|
|
171
172
|
const op = {
|
|
@@ -174,10 +175,16 @@ export class TaskManagerClass extends SharedObject {
|
|
|
174
175
|
};
|
|
175
176
|
const pendingOp = {
|
|
176
177
|
type: "abandon",
|
|
177
|
-
messageId:
|
|
178
|
+
messageId: this.nextPendingMessageId++,
|
|
178
179
|
};
|
|
179
180
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
180
|
-
this.latestPendingOps.
|
|
181
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
182
|
+
if (latestPendingOps === undefined) {
|
|
183
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
latestPendingOps.push(pendingOp);
|
|
187
|
+
}
|
|
181
188
|
}
|
|
182
189
|
submitCompleteOp(taskId) {
|
|
183
190
|
const op = {
|
|
@@ -186,23 +193,25 @@ export class TaskManagerClass extends SharedObject {
|
|
|
186
193
|
};
|
|
187
194
|
const pendingOp = {
|
|
188
195
|
type: "complete",
|
|
189
|
-
messageId:
|
|
196
|
+
messageId: this.nextPendingMessageId++,
|
|
190
197
|
};
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
this.submitLocalMessage(op, pendingOp.messageId);
|
|
199
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
200
|
+
if (latestPendingOps === undefined) {
|
|
201
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
193
202
|
}
|
|
194
203
|
else {
|
|
195
|
-
|
|
204
|
+
latestPendingOps.push(pendingOp);
|
|
196
205
|
}
|
|
197
|
-
this.submitLocalMessage(op, pendingOp.messageId);
|
|
198
|
-
this.latestPendingOps.set(taskId, pendingOp);
|
|
199
206
|
}
|
|
200
207
|
/**
|
|
201
208
|
* {@inheritDoc ITaskManager.volunteerForTask}
|
|
202
209
|
*/
|
|
203
210
|
async volunteerForTask(taskId) {
|
|
204
|
-
// If we have the lock
|
|
205
|
-
|
|
211
|
+
// If we are both queued and assigned, then we have the lock and do not
|
|
212
|
+
// have any pending abandon/complete ops. In this case we can resolve
|
|
213
|
+
// true immediately.
|
|
214
|
+
if (this.queuedOptimistically(taskId) && this.assigned(taskId)) {
|
|
206
215
|
return true;
|
|
207
216
|
}
|
|
208
217
|
if (this.readOnlyInfo.readonly === true) {
|
|
@@ -211,7 +220,7 @@ export class TaskManagerClass extends SharedObject {
|
|
|
211
220
|
: new Error("Attempted to volunteer in read-only state");
|
|
212
221
|
throw error;
|
|
213
222
|
}
|
|
214
|
-
if (
|
|
223
|
+
if (this.isDetached()) {
|
|
215
224
|
// Simulate auto-ack in detached scenario
|
|
216
225
|
assert(this.clientId !== undefined, 0x472 /* clientId should not be undefined */);
|
|
217
226
|
this.addClientToQueue(taskId, this.clientId);
|
|
@@ -222,6 +231,25 @@ export class TaskManagerClass extends SharedObject {
|
|
|
222
231
|
}
|
|
223
232
|
// This promise works even if we already have an outstanding volunteer op.
|
|
224
233
|
const lockAcquireP = new Promise((resolve, reject) => {
|
|
234
|
+
// If we don't send an op (meaning the latest pending op is "volunteer"), nextPendingMessageId
|
|
235
|
+
// will be greater than that prior "volunteer" op's messageId. This is OK because
|
|
236
|
+
// we only use it to filter stale abandon/complete, and not when determining if we
|
|
237
|
+
// acquired the lock.
|
|
238
|
+
const nextPendingMessageId = this.nextPendingMessageId;
|
|
239
|
+
const setupListeners = () => {
|
|
240
|
+
this.queueWatcher.on("queueChange", checkIfAcquiredLock);
|
|
241
|
+
this.abandonWatcher.on("abandon", checkIfAbandoned);
|
|
242
|
+
this.connectionWatcher.on("disconnect", rejectOnDisconnect);
|
|
243
|
+
this.completedWatcher.on("completed", checkIfCompleted);
|
|
244
|
+
this.rollbackWatcher.on("rollback", checkIfRolledBack);
|
|
245
|
+
};
|
|
246
|
+
const removeListeners = () => {
|
|
247
|
+
this.queueWatcher.off("queueChange", checkIfAcquiredLock);
|
|
248
|
+
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
249
|
+
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
250
|
+
this.completedWatcher.off("completed", checkIfCompleted);
|
|
251
|
+
this.rollbackWatcher.off("rollback", checkIfRolledBack);
|
|
252
|
+
};
|
|
225
253
|
const checkIfAcquiredLock = (eventTaskId) => {
|
|
226
254
|
if (eventTaskId !== taskId) {
|
|
227
255
|
return;
|
|
@@ -229,47 +257,48 @@ export class TaskManagerClass extends SharedObject {
|
|
|
229
257
|
// Also check pending ops here because it's possible we are currently in the queue from a previous
|
|
230
258
|
// lock attempt, but have an outstanding abandon AND the outstanding volunteer for this lock attempt.
|
|
231
259
|
// If we reach the head of the queue based on the previous lock attempt, we don't want to resolve.
|
|
232
|
-
if (this.assigned(taskId)
|
|
233
|
-
|
|
234
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
235
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
236
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
260
|
+
if (this.assigned(taskId)) {
|
|
261
|
+
removeListeners();
|
|
237
262
|
resolve(true);
|
|
238
263
|
}
|
|
239
264
|
};
|
|
240
|
-
const checkIfAbandoned = (eventTaskId) => {
|
|
265
|
+
const checkIfAbandoned = (eventTaskId, messageId) => {
|
|
241
266
|
if (eventTaskId !== taskId) {
|
|
242
267
|
return;
|
|
243
268
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
269
|
+
if (messageId !== undefined && messageId <= nextPendingMessageId) {
|
|
270
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
removeListeners();
|
|
248
274
|
reject(new Error("Abandoned before acquiring task assignment"));
|
|
249
275
|
};
|
|
250
276
|
const rejectOnDisconnect = () => {
|
|
251
|
-
|
|
252
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
253
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
254
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
277
|
+
removeListeners();
|
|
255
278
|
reject(new Error("Disconnected before acquiring task assignment"));
|
|
256
279
|
};
|
|
257
|
-
const checkIfCompleted = (eventTaskId) => {
|
|
280
|
+
const checkIfCompleted = (eventTaskId, messageId) => {
|
|
258
281
|
if (eventTaskId !== taskId) {
|
|
259
282
|
return;
|
|
260
283
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
if (messageId !== undefined && messageId <= nextPendingMessageId) {
|
|
285
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
removeListeners();
|
|
265
289
|
resolve(false);
|
|
266
290
|
};
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
const checkIfRolledBack = (eventTaskId) => {
|
|
292
|
+
if (eventTaskId !== taskId) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
removeListeners();
|
|
296
|
+
resolve(false);
|
|
297
|
+
};
|
|
298
|
+
setupListeners();
|
|
271
299
|
});
|
|
272
|
-
if (!this.
|
|
300
|
+
if (!this.queuedOptimistically(taskId)) {
|
|
301
|
+
// Only send the volunteer op if we are not already queued.
|
|
273
302
|
this.submitVolunteerOp(taskId);
|
|
274
303
|
}
|
|
275
304
|
return lockAcquireP;
|
|
@@ -284,37 +313,69 @@ export class TaskManagerClass extends SharedObject {
|
|
|
284
313
|
if (this.readOnlyInfo.readonly === true && this.readOnlyInfo.permissions === true) {
|
|
285
314
|
throw new Error("Attempted to subscribe with read-only permissions");
|
|
286
315
|
}
|
|
316
|
+
let volunteerOpMessageId;
|
|
287
317
|
const submitVolunteerOp = () => {
|
|
318
|
+
volunteerOpMessageId = this.nextPendingMessageId;
|
|
288
319
|
this.submitVolunteerOp(taskId);
|
|
289
320
|
};
|
|
321
|
+
const setupListeners = () => {
|
|
322
|
+
this.abandonWatcher.on("abandon", checkIfAbandoned);
|
|
323
|
+
this.connectionWatcher.on("disconnect", disconnectHandler);
|
|
324
|
+
this.completedWatcher.on("completed", checkIfCompleted);
|
|
325
|
+
this.rollbackWatcher.on("rollback", checkIfRolledBack);
|
|
326
|
+
};
|
|
327
|
+
const removeListeners = () => {
|
|
328
|
+
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
329
|
+
this.connectionWatcher.off("disconnect", disconnectHandler);
|
|
330
|
+
this.connectionWatcher.off("connect", submitVolunteerOp);
|
|
331
|
+
this.completedWatcher.off("completed", checkIfCompleted);
|
|
332
|
+
this.rollbackWatcher.off("rollback", checkIfRolledBack);
|
|
333
|
+
};
|
|
290
334
|
const disconnectHandler = () => {
|
|
291
335
|
// Wait to be connected again and then re-submit volunteer op
|
|
292
336
|
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
293
337
|
};
|
|
294
|
-
const checkIfAbandoned = (eventTaskId) => {
|
|
338
|
+
const checkIfAbandoned = (eventTaskId, messageId) => {
|
|
295
339
|
if (eventTaskId !== taskId) {
|
|
296
340
|
return;
|
|
297
341
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.
|
|
301
|
-
|
|
342
|
+
// abandonWatcher emits twice for a local abandon() call. When initially called it
|
|
343
|
+
// will emit with undefined messageId. It will emit a second time when the op is
|
|
344
|
+
// ack'd and processed, this time with the messageId for the ack.
|
|
345
|
+
// This condition accounts ensures we don't ignore the initial abandon() emit and
|
|
346
|
+
// only ignore emits associated with ack'd abandon ops that were sent prior to the
|
|
347
|
+
// current volunteer attempt.
|
|
348
|
+
if (messageId !== undefined &&
|
|
349
|
+
volunteerOpMessageId !== undefined &&
|
|
350
|
+
messageId <= volunteerOpMessageId) {
|
|
351
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
removeListeners();
|
|
302
355
|
this.subscribedTasks.delete(taskId);
|
|
303
356
|
};
|
|
304
|
-
const checkIfCompleted = (eventTaskId) => {
|
|
357
|
+
const checkIfCompleted = (eventTaskId, messageId) => {
|
|
305
358
|
if (eventTaskId !== taskId) {
|
|
306
359
|
return;
|
|
307
360
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
if (messageId !== undefined &&
|
|
362
|
+
volunteerOpMessageId !== undefined &&
|
|
363
|
+
messageId <= volunteerOpMessageId) {
|
|
364
|
+
// Ignore abandon events that were for abandon ops that were sent prior to our current volunteer attempt.
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
removeListeners();
|
|
368
|
+
this.subscribedTasks.delete(taskId);
|
|
369
|
+
};
|
|
370
|
+
const checkIfRolledBack = (eventTaskId) => {
|
|
371
|
+
if (eventTaskId !== taskId) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
removeListeners();
|
|
312
375
|
this.subscribedTasks.delete(taskId);
|
|
313
376
|
};
|
|
314
|
-
|
|
315
|
-
this.
|
|
316
|
-
this.completedWatcher.on("completed", checkIfCompleted);
|
|
317
|
-
if (!this.isAttached()) {
|
|
377
|
+
setupListeners();
|
|
378
|
+
if (this.isDetached()) {
|
|
318
379
|
// Simulate auto-ack in detached scenario
|
|
319
380
|
assert(this.clientId !== undefined, 0x473 /* clientId should not be undefined */);
|
|
320
381
|
this.addClientToQueue(taskId, this.clientId);
|
|
@@ -322,18 +383,13 @@ export class TaskManagerClass extends SharedObject {
|
|
|
322
383
|
// a real clientId. At that point we should re-enter the queue with a real volunteer op (assuming we are
|
|
323
384
|
// connected).
|
|
324
385
|
this.runtime.once("attached", () => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
else if (this.connected) {
|
|
386
|
+
// We call scrubClientsNotInQuorum() in case our clientId changed during the attach process.
|
|
387
|
+
this.scrubClientsNotInQuorum();
|
|
388
|
+
if (this.connected) {
|
|
331
389
|
submitVolunteerOp();
|
|
332
390
|
}
|
|
333
391
|
else {
|
|
334
|
-
this.connectionWatcher.once("connect",
|
|
335
|
-
submitVolunteerOp();
|
|
336
|
-
});
|
|
392
|
+
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
337
393
|
}
|
|
338
394
|
});
|
|
339
395
|
}
|
|
@@ -341,7 +397,8 @@ export class TaskManagerClass extends SharedObject {
|
|
|
341
397
|
// If we are disconnected (and attached), wait to be connected and submit volunteer op
|
|
342
398
|
disconnectHandler();
|
|
343
399
|
}
|
|
344
|
-
else if (!this.
|
|
400
|
+
else if (!this.queuedOptimistically(taskId)) {
|
|
401
|
+
// We don't need to send a second volunteer op if we just sent one.
|
|
345
402
|
submitVolunteerOp();
|
|
346
403
|
}
|
|
347
404
|
this.subscribedTasks.add(taskId);
|
|
@@ -351,23 +408,19 @@ export class TaskManagerClass extends SharedObject {
|
|
|
351
408
|
*/
|
|
352
409
|
abandon(taskId) {
|
|
353
410
|
// Always allow abandon if the client is subscribed to allow clients to unsubscribe while disconnected.
|
|
354
|
-
// Otherwise, we should check to make sure the client is
|
|
355
|
-
|
|
356
|
-
if (!this.subscribed(taskId) && !this.queued(taskId)) {
|
|
411
|
+
// Otherwise, we should check to make sure the client is optimistically queued for the task before trying to abandon.
|
|
412
|
+
if (!this.queuedOptimistically(taskId) && !this.subscribed(taskId)) {
|
|
357
413
|
// Nothing to do
|
|
358
414
|
return;
|
|
359
415
|
}
|
|
360
|
-
if (
|
|
416
|
+
if (this.isDetached()) {
|
|
361
417
|
// Simulate auto-ack in detached scenario
|
|
362
418
|
assert(this.clientId !== undefined, 0x474 /* clientId is undefined */);
|
|
363
419
|
this.removeClientFromQueue(taskId, this.clientId);
|
|
364
420
|
this.abandonWatcher.emit("abandon", taskId);
|
|
365
421
|
return;
|
|
366
422
|
}
|
|
367
|
-
|
|
368
|
-
if (this.queued(taskId)) {
|
|
369
|
-
this.submitAbandonOp(taskId);
|
|
370
|
-
}
|
|
423
|
+
this.submitAbandonOp(taskId);
|
|
371
424
|
this.abandonWatcher.emit("abandon", taskId);
|
|
372
425
|
}
|
|
373
426
|
/**
|
|
@@ -378,9 +431,7 @@ export class TaskManagerClass extends SharedObject {
|
|
|
378
431
|
return false;
|
|
379
432
|
}
|
|
380
433
|
const currentAssignee = this.taskQueues.get(taskId)?.[0];
|
|
381
|
-
return
|
|
382
|
-
currentAssignee === this.clientId &&
|
|
383
|
-
!this.latestPendingOps.has(taskId));
|
|
434
|
+
return currentAssignee !== undefined && currentAssignee === this.clientId;
|
|
384
435
|
}
|
|
385
436
|
/**
|
|
386
437
|
* {@inheritDoc ITaskManager.queued}
|
|
@@ -390,11 +441,7 @@ export class TaskManagerClass extends SharedObject {
|
|
|
390
441
|
return false;
|
|
391
442
|
}
|
|
392
443
|
assert(this.clientId !== undefined, 0x07f /* "clientId undefined" */);
|
|
393
|
-
|
|
394
|
-
// If we have no queue for the taskId, then no one has signed up for it.
|
|
395
|
-
return (((clientQueue?.includes(this.clientId) ?? false) &&
|
|
396
|
-
!this.latestPendingOps.has(taskId)) ||
|
|
397
|
-
this.latestPendingOps.get(taskId)?.type === "volunteer");
|
|
444
|
+
return this.taskQueues.get(taskId)?.includes(this.clientId) ?? false;
|
|
398
445
|
}
|
|
399
446
|
/**
|
|
400
447
|
* {@inheritDoc ITaskManager.subscribed}
|
|
@@ -411,15 +458,16 @@ export class TaskManagerClass extends SharedObject {
|
|
|
411
458
|
}
|
|
412
459
|
// If we are detached we will simulate auto-ack for the complete op. Therefore we only need to send the op if
|
|
413
460
|
// we are attached. Additionally, we don't need to check if we are connected while detached.
|
|
414
|
-
if (this.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
461
|
+
if (this.isDetached()) {
|
|
462
|
+
this.taskQueues.delete(taskId);
|
|
463
|
+
this.completedWatcher.emit("completed", taskId);
|
|
464
|
+
this.emit("completed", taskId);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!this.connected) {
|
|
468
|
+
throw new Error("Attempted to complete task in disconnected state");
|
|
419
469
|
}
|
|
420
|
-
this.
|
|
421
|
-
this.completedWatcher.emit("completed", taskId);
|
|
422
|
-
this.emit("completed", taskId);
|
|
470
|
+
this.submitCompleteOp(taskId);
|
|
423
471
|
}
|
|
424
472
|
/**
|
|
425
473
|
* {@inheritDoc ITaskManager.canVolunteer}
|
|
@@ -482,12 +530,22 @@ export class TaskManagerClass extends SharedObject {
|
|
|
482
530
|
onConnect() {
|
|
483
531
|
this.connectionWatcher.emit("connect");
|
|
484
532
|
}
|
|
485
|
-
//
|
|
486
533
|
/**
|
|
487
534
|
* Override resubmit core to avoid resubmission on reconnect. On disconnect we accept our removal from the
|
|
488
535
|
* queues, and leave it up to the user to decide whether they want to attempt to re-enter a queue on reconnect.
|
|
536
|
+
* However, we do need to update latestPendingOps to account for the ops we will no longer be processing.
|
|
489
537
|
*/
|
|
490
|
-
reSubmitCore() {
|
|
538
|
+
reSubmitCore(content, localOpMetadata) {
|
|
539
|
+
assertIsTaskManagerOperation(content);
|
|
540
|
+
const pendingOps = this.latestPendingOps.get(content.taskId);
|
|
541
|
+
assert(pendingOps !== undefined, "No pending ops for task on resubmit attempt");
|
|
542
|
+
const pendingOpIndex = pendingOps.findIndex((op) => op.messageId === localOpMetadata && op.type === content.type);
|
|
543
|
+
assert(pendingOpIndex !== -1, "Could not match pending op on resubmit attempt");
|
|
544
|
+
pendingOps.splice(pendingOpIndex, 1);
|
|
545
|
+
if (pendingOps.length === 0) {
|
|
546
|
+
this.latestPendingOps.delete(content.taskId);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
491
549
|
/**
|
|
492
550
|
* Process a task manager operation
|
|
493
551
|
*
|
|
@@ -521,11 +579,6 @@ export class TaskManagerClass extends SharedObject {
|
|
|
521
579
|
}
|
|
522
580
|
}
|
|
523
581
|
addClientToQueue(taskId, clientId) {
|
|
524
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
525
|
-
if (pendingIds !== undefined && pendingIds.length > 0) {
|
|
526
|
-
// Ignore the volunteer op if we know this task is about to be completed
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
582
|
// Ensure that the clientId exists in the quorum, or it is placeholderClientId (detached scenario)
|
|
530
583
|
if (this.runtime.getQuorum().getMembers().has(clientId) ||
|
|
531
584
|
this.clientId === placeholderClientId) {
|
|
@@ -535,6 +588,12 @@ export class TaskManagerClass extends SharedObject {
|
|
|
535
588
|
clientQueue = [];
|
|
536
589
|
this.taskQueues.set(taskId, clientQueue);
|
|
537
590
|
}
|
|
591
|
+
if (clientQueue.includes(clientId)) {
|
|
592
|
+
// We shouldn't re-add the client if it's already in the queue.
|
|
593
|
+
// This may be possible in scenarios where a client was added in
|
|
594
|
+
// while detached.
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
538
597
|
const oldLockHolder = clientQueue[0];
|
|
539
598
|
clientQueue.push(clientId);
|
|
540
599
|
const newLockHolder = clientQueue[0];
|
|
@@ -576,7 +635,13 @@ export class TaskManagerClass extends SharedObject {
|
|
|
576
635
|
for (const clientQueue of this.taskQueues.values()) {
|
|
577
636
|
const clientIdIndex = clientQueue.indexOf(placeholderClientId);
|
|
578
637
|
if (clientIdIndex !== -1) {
|
|
579
|
-
clientQueue
|
|
638
|
+
if (clientQueue.includes(this.runtime.clientId)) {
|
|
639
|
+
// If the real clientId is already in the queue, just remove the placeholder.
|
|
640
|
+
clientQueue.splice(clientIdIndex, 1);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
clientQueue[clientIdIndex] = this.runtime.clientId;
|
|
644
|
+
}
|
|
580
645
|
}
|
|
581
646
|
}
|
|
582
647
|
}
|
|
@@ -597,25 +662,55 @@ export class TaskManagerClass extends SharedObject {
|
|
|
597
662
|
}
|
|
598
663
|
}
|
|
599
664
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
case "complete": {
|
|
608
|
-
this.complete(taskOp.taskId);
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
case "volunteer": {
|
|
612
|
-
this.subscribeToTask(taskOp.taskId);
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
default: {
|
|
616
|
-
unreachableCase(taskOp);
|
|
617
|
-
}
|
|
665
|
+
/**
|
|
666
|
+
* Checks whether this client is currently assigned or in queue to become assigned, while also accounting
|
|
667
|
+
* for the latest pending ops.
|
|
668
|
+
*/
|
|
669
|
+
queuedOptimistically(taskId) {
|
|
670
|
+
if (this.isAttached() && !this.connected) {
|
|
671
|
+
return false;
|
|
618
672
|
}
|
|
673
|
+
assert(this.clientId !== undefined, 0x07f /* "clientId undefined" */);
|
|
674
|
+
const inQueue = this.taskQueues.get(taskId)?.includes(this.clientId) ?? false;
|
|
675
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
676
|
+
const latestPendingOp = latestPendingOps !== undefined && latestPendingOps.length > 0
|
|
677
|
+
? latestPendingOps[latestPendingOps.length - 1]
|
|
678
|
+
: undefined;
|
|
679
|
+
const isPendingVolunteer = latestPendingOp?.type === "volunteer";
|
|
680
|
+
const isPendingAbandonOrComplete = latestPendingOp?.type === "abandon" || latestPendingOp?.type === "complete";
|
|
681
|
+
// We return true if the client is either in queue already or the latest pending op for this task is a volunteer op.
|
|
682
|
+
// But we should always return false if the latest pending op is an abandon or complete op.
|
|
683
|
+
return (inQueue && !isPendingAbandonOrComplete) || isPendingVolunteer;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Returns true if the client is detached.
|
|
687
|
+
* This is distinct from !this.isAttached() because `isAttached()` also checks if `this._isBoundToContext`
|
|
688
|
+
* is true. We use `isDetached()` to determine if we should simulate auto-ack behavior for ops, which is
|
|
689
|
+
* mainly concerned with if we have been assigned a real clientId yet.
|
|
690
|
+
*/
|
|
691
|
+
isDetached() {
|
|
692
|
+
return this.runtime.attachState === AttachState.Detached;
|
|
693
|
+
}
|
|
694
|
+
applyStashedOp(content) {
|
|
695
|
+
// We don't apply any stashed ops since during the rehydration process. Since we lose any assigned tasks
|
|
696
|
+
// during rehydration we cannot be assigned any tasks. Additionally, without the in-memory state of the
|
|
697
|
+
// previous dds, we also cannot re-volunteer based on a previous subscribeToTask() call. Since we are
|
|
698
|
+
// unable to be assigned to any tasks, there is no reason to process abandon/complete ops either.
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
702
|
+
*/
|
|
703
|
+
rollback(content, localOpMetadata) {
|
|
704
|
+
assert(typeof localOpMetadata === "number", "Expect localOpMetadata to be a number");
|
|
705
|
+
assertIsTaskManagerOperation(content);
|
|
706
|
+
const latestPendingOps = this.latestPendingOps.get(content.taskId);
|
|
707
|
+
assert(latestPendingOps !== undefined, "No pending ops when trying to rollback");
|
|
708
|
+
const pendingOpToRollback = latestPendingOps.pop();
|
|
709
|
+
assert(pendingOpToRollback !== undefined && pendingOpToRollback.messageId === localOpMetadata, "pending op mismatch");
|
|
710
|
+
if (latestPendingOps.length === 0) {
|
|
711
|
+
this.latestPendingOps.delete(content.taskId);
|
|
712
|
+
}
|
|
713
|
+
this.rollbackWatcher.emit("rollback", content.taskId);
|
|
619
714
|
}
|
|
620
715
|
}
|
|
621
716
|
//# sourceMappingURL=taskManager.js.map
|