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