@fluidframework/task-manager 2.60.0 → 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/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/internal.d.ts +1 -1
- package/legacy.d.ts +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/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.
|
|
@@ -21,7 +30,7 @@ const placeholderClientId = "placeholder";
|
|
|
21
30
|
* @sealed
|
|
22
31
|
* @legacy @beta
|
|
23
32
|
*/
|
|
24
|
-
class TaskManagerClass extends
|
|
33
|
+
class TaskManagerClass extends internal_5.SharedObject {
|
|
25
34
|
/**
|
|
26
35
|
* Returns the clientId. Will return a placeholder if the runtime is detached and not yet assigned a clientId.
|
|
27
36
|
*/
|
|
@@ -58,7 +67,9 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
58
67
|
this.connectionWatcher = new client_utils_1.EventEmitter();
|
|
59
68
|
// completedWatcher emits an event whenever the local client receives a completed op.
|
|
60
69
|
this.completedWatcher = new client_utils_1.EventEmitter();
|
|
61
|
-
|
|
70
|
+
// rollbackWatcher emits an event whenever a pending op is rolled back.
|
|
71
|
+
this.rollbackWatcher = new client_utils_1.EventEmitter();
|
|
72
|
+
this.nextPendingMessageId = 0;
|
|
62
73
|
/**
|
|
63
74
|
* Tracks the most recent pending op for a given task
|
|
64
75
|
*/
|
|
@@ -67,60 +78,47 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
67
78
|
* Tracks tasks that are this client is currently subscribed to.
|
|
68
79
|
*/
|
|
69
80
|
this.subscribedTasks = new Set();
|
|
70
|
-
/**
|
|
71
|
-
* Map to track tasks that have pending complete ops.
|
|
72
|
-
*/
|
|
73
|
-
this.pendingCompletedTasks = new Map();
|
|
74
81
|
this.opWatcher.on("volunteer", (taskId, clientId, local, messageId) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const pendingOp =
|
|
79
|
-
(0,
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
(0, internal_1.assert)(pendingOp.type === "volunteer", 0x07c /* "Unexpected op type" */);
|
|
83
|
-
// 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) {
|
|
84
89
|
this.latestPendingOps.delete(taskId);
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
this.addClientToQueue(taskId, clientId);
|
|
88
93
|
});
|
|
89
94
|
this.opWatcher.on("abandon", (taskId, clientId, local, messageId) => {
|
|
90
|
-
if (
|
|
91
|
-
const
|
|
92
|
-
(0,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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) {
|
|
97
102
|
this.latestPendingOps.delete(taskId);
|
|
98
103
|
}
|
|
104
|
+
this.abandonWatcher.emit("abandon", taskId, messageId);
|
|
99
105
|
}
|
|
100
106
|
this.removeClientFromQueue(taskId, clientId);
|
|
101
107
|
});
|
|
102
108
|
this.opWatcher.on("complete", (taskId, clientId, local, messageId) => {
|
|
103
|
-
if (
|
|
104
|
-
const
|
|
105
|
-
(0,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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) {
|
|
110
116
|
this.latestPendingOps.delete(taskId);
|
|
111
117
|
}
|
|
112
|
-
// Remove complete op from this.pendingCompletedTasks
|
|
113
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
114
|
-
(0, internal_1.assert)(pendingIds !== undefined && pendingIds.length > 0, 0x402 /* pendingIds is empty */);
|
|
115
|
-
const removed = pendingIds.shift();
|
|
116
|
-
(0, internal_1.assert)(removed === messageId, 0x403 /* Removed complete op id does not match */);
|
|
117
|
-
}
|
|
118
|
-
// For clients in queue, we need to remove them from the queue and raise the proper events.
|
|
119
|
-
if (!local) {
|
|
120
|
-
this.taskQueues.delete(taskId);
|
|
121
|
-
this.completedWatcher.emit("completed", taskId);
|
|
122
|
-
this.emit("completed", taskId);
|
|
123
118
|
}
|
|
119
|
+
this.taskQueues.delete(taskId);
|
|
120
|
+
this.completedWatcher.emit("completed", taskId, messageId);
|
|
121
|
+
this.emit("completed", taskId);
|
|
124
122
|
});
|
|
125
123
|
runtime.getQuorum().on("removeMember", (clientId) => {
|
|
126
124
|
this.removeClientFromAllQueues(clientId);
|
|
@@ -143,18 +141,16 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
143
141
|
}
|
|
144
142
|
});
|
|
145
143
|
this.connectionWatcher.on("disconnect", () => {
|
|
146
|
-
(0,
|
|
147
|
-
//
|
|
148
|
-
// After reconnect these will get cleaned up by observing the clientLeaves.
|
|
149
|
-
// However we do need to recognize that we lost the lock if we had it. Calls to .queued() and
|
|
150
|
-
// .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.
|
|
151
146
|
for (const [taskId, clientQueue] of this.taskQueues.entries()) {
|
|
152
147
|
if (this.isAttached() && clientQueue[0] === this.clientId) {
|
|
153
148
|
this.emit("lost", taskId);
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
|
-
//
|
|
157
|
-
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);
|
|
158
154
|
});
|
|
159
155
|
}
|
|
160
156
|
submitVolunteerOp(taskId) {
|
|
@@ -164,10 +160,16 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
164
160
|
};
|
|
165
161
|
const pendingOp = {
|
|
166
162
|
type: "volunteer",
|
|
167
|
-
messageId:
|
|
163
|
+
messageId: this.nextPendingMessageId++,
|
|
168
164
|
};
|
|
169
165
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
170
|
-
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
|
+
}
|
|
171
173
|
}
|
|
172
174
|
submitAbandonOp(taskId) {
|
|
173
175
|
const op = {
|
|
@@ -176,10 +178,16 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
176
178
|
};
|
|
177
179
|
const pendingOp = {
|
|
178
180
|
type: "abandon",
|
|
179
|
-
messageId:
|
|
181
|
+
messageId: this.nextPendingMessageId++,
|
|
180
182
|
};
|
|
181
183
|
this.submitLocalMessage(op, pendingOp.messageId);
|
|
182
|
-
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
|
+
}
|
|
183
191
|
}
|
|
184
192
|
submitCompleteOp(taskId) {
|
|
185
193
|
const op = {
|
|
@@ -188,23 +196,25 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
188
196
|
};
|
|
189
197
|
const pendingOp = {
|
|
190
198
|
type: "complete",
|
|
191
|
-
messageId:
|
|
199
|
+
messageId: this.nextPendingMessageId++,
|
|
192
200
|
};
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
this.submitLocalMessage(op, pendingOp.messageId);
|
|
202
|
+
const latestPendingOps = this.latestPendingOps.get(taskId);
|
|
203
|
+
if (latestPendingOps === undefined) {
|
|
204
|
+
this.latestPendingOps.set(taskId, [pendingOp]);
|
|
195
205
|
}
|
|
196
206
|
else {
|
|
197
|
-
|
|
207
|
+
latestPendingOps.push(pendingOp);
|
|
198
208
|
}
|
|
199
|
-
this.submitLocalMessage(op, pendingOp.messageId);
|
|
200
|
-
this.latestPendingOps.set(taskId, pendingOp);
|
|
201
209
|
}
|
|
202
210
|
/**
|
|
203
211
|
* {@inheritDoc ITaskManager.volunteerForTask}
|
|
204
212
|
*/
|
|
205
213
|
async volunteerForTask(taskId) {
|
|
206
|
-
// If we have the lock
|
|
207
|
-
|
|
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)) {
|
|
208
218
|
return true;
|
|
209
219
|
}
|
|
210
220
|
if (this.readOnlyInfo.readonly === true) {
|
|
@@ -213,9 +223,9 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
213
223
|
: new Error("Attempted to volunteer in read-only state");
|
|
214
224
|
throw error;
|
|
215
225
|
}
|
|
216
|
-
if (
|
|
226
|
+
if (this.isDetached()) {
|
|
217
227
|
// Simulate auto-ack in detached scenario
|
|
218
|
-
(0,
|
|
228
|
+
(0, internal_2.assert)(this.clientId !== undefined, 0x472 /* clientId should not be undefined */);
|
|
219
229
|
this.addClientToQueue(taskId, this.clientId);
|
|
220
230
|
return true;
|
|
221
231
|
}
|
|
@@ -224,6 +234,25 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
224
234
|
}
|
|
225
235
|
// This promise works even if we already have an outstanding volunteer op.
|
|
226
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
|
+
};
|
|
227
256
|
const checkIfAcquiredLock = (eventTaskId) => {
|
|
228
257
|
if (eventTaskId !== taskId) {
|
|
229
258
|
return;
|
|
@@ -231,47 +260,48 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
231
260
|
// Also check pending ops here because it's possible we are currently in the queue from a previous
|
|
232
261
|
// lock attempt, but have an outstanding abandon AND the outstanding volunteer for this lock attempt.
|
|
233
262
|
// If we reach the head of the queue based on the previous lock attempt, we don't want to resolve.
|
|
234
|
-
if (this.assigned(taskId)
|
|
235
|
-
|
|
236
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
237
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
238
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
263
|
+
if (this.assigned(taskId)) {
|
|
264
|
+
removeListeners();
|
|
239
265
|
resolve(true);
|
|
240
266
|
}
|
|
241
267
|
};
|
|
242
|
-
const checkIfAbandoned = (eventTaskId) => {
|
|
268
|
+
const checkIfAbandoned = (eventTaskId, messageId) => {
|
|
243
269
|
if (eventTaskId !== taskId) {
|
|
244
270
|
return;
|
|
245
271
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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();
|
|
250
277
|
reject(new Error("Abandoned before acquiring task assignment"));
|
|
251
278
|
};
|
|
252
279
|
const rejectOnDisconnect = () => {
|
|
253
|
-
|
|
254
|
-
this.abandonWatcher.off("abandon", checkIfAbandoned);
|
|
255
|
-
this.connectionWatcher.off("disconnect", rejectOnDisconnect);
|
|
256
|
-
this.completedWatcher.off("completed", checkIfCompleted);
|
|
280
|
+
removeListeners();
|
|
257
281
|
reject(new Error("Disconnected before acquiring task assignment"));
|
|
258
282
|
};
|
|
259
|
-
const checkIfCompleted = (eventTaskId) => {
|
|
283
|
+
const checkIfCompleted = (eventTaskId, messageId) => {
|
|
260
284
|
if (eventTaskId !== taskId) {
|
|
261
285
|
return;
|
|
262
286
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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();
|
|
267
292
|
resolve(false);
|
|
268
293
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
const checkIfRolledBack = (eventTaskId) => {
|
|
295
|
+
if (eventTaskId !== taskId) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
removeListeners();
|
|
299
|
+
resolve(false);
|
|
300
|
+
};
|
|
301
|
+
setupListeners();
|
|
273
302
|
});
|
|
274
|
-
if (!this.
|
|
303
|
+
if (!this.queuedOptimistically(taskId)) {
|
|
304
|
+
// Only send the volunteer op if we are not already queued.
|
|
275
305
|
this.submitVolunteerOp(taskId);
|
|
276
306
|
}
|
|
277
307
|
return lockAcquireP;
|
|
@@ -286,56 +316,83 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
286
316
|
if (this.readOnlyInfo.readonly === true && this.readOnlyInfo.permissions === true) {
|
|
287
317
|
throw new Error("Attempted to subscribe with read-only permissions");
|
|
288
318
|
}
|
|
319
|
+
let volunteerOpMessageId;
|
|
289
320
|
const submitVolunteerOp = () => {
|
|
321
|
+
volunteerOpMessageId = this.nextPendingMessageId;
|
|
290
322
|
this.submitVolunteerOp(taskId);
|
|
291
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
|
+
};
|
|
292
337
|
const disconnectHandler = () => {
|
|
293
338
|
// Wait to be connected again and then re-submit volunteer op
|
|
294
339
|
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
295
340
|
};
|
|
296
|
-
const checkIfAbandoned = (eventTaskId) => {
|
|
341
|
+
const checkIfAbandoned = (eventTaskId, messageId) => {
|
|
297
342
|
if (eventTaskId !== taskId) {
|
|
298
343
|
return;
|
|
299
344
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
|
|
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();
|
|
304
358
|
this.subscribedTasks.delete(taskId);
|
|
305
359
|
};
|
|
306
|
-
const checkIfCompleted = (eventTaskId) => {
|
|
360
|
+
const checkIfCompleted = (eventTaskId, messageId) => {
|
|
307
361
|
if (eventTaskId !== taskId) {
|
|
308
362
|
return;
|
|
309
363
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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();
|
|
314
371
|
this.subscribedTasks.delete(taskId);
|
|
315
372
|
};
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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()) {
|
|
320
382
|
// Simulate auto-ack in detached scenario
|
|
321
|
-
(0,
|
|
383
|
+
(0, internal_2.assert)(this.clientId !== undefined, 0x473 /* clientId should not be undefined */);
|
|
322
384
|
this.addClientToQueue(taskId, this.clientId);
|
|
323
385
|
// Because we volunteered with placeholderClientId, we need to wait for when we attach and are assigned
|
|
324
386
|
// a real clientId. At that point we should re-enter the queue with a real volunteer op (assuming we are
|
|
325
387
|
// connected).
|
|
326
388
|
this.runtime.once("attached", () => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
else if (this.connected) {
|
|
389
|
+
// We call scrubClientsNotInQuorum() in case our clientId changed during the attach process.
|
|
390
|
+
this.scrubClientsNotInQuorum();
|
|
391
|
+
if (this.connected) {
|
|
333
392
|
submitVolunteerOp();
|
|
334
393
|
}
|
|
335
394
|
else {
|
|
336
|
-
this.connectionWatcher.once("connect",
|
|
337
|
-
submitVolunteerOp();
|
|
338
|
-
});
|
|
395
|
+
this.connectionWatcher.once("connect", submitVolunteerOp);
|
|
339
396
|
}
|
|
340
397
|
});
|
|
341
398
|
}
|
|
@@ -343,7 +400,8 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
343
400
|
// If we are disconnected (and attached), wait to be connected and submit volunteer op
|
|
344
401
|
disconnectHandler();
|
|
345
402
|
}
|
|
346
|
-
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.
|
|
347
405
|
submitVolunteerOp();
|
|
348
406
|
}
|
|
349
407
|
this.subscribedTasks.add(taskId);
|
|
@@ -353,23 +411,19 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
353
411
|
*/
|
|
354
412
|
abandon(taskId) {
|
|
355
413
|
// Always allow abandon if the client is subscribed to allow clients to unsubscribe while disconnected.
|
|
356
|
-
// Otherwise, we should check to make sure the client is
|
|
357
|
-
|
|
358
|
-
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)) {
|
|
359
416
|
// Nothing to do
|
|
360
417
|
return;
|
|
361
418
|
}
|
|
362
|
-
if (
|
|
419
|
+
if (this.isDetached()) {
|
|
363
420
|
// Simulate auto-ack in detached scenario
|
|
364
|
-
(0,
|
|
421
|
+
(0, internal_2.assert)(this.clientId !== undefined, 0x474 /* clientId is undefined */);
|
|
365
422
|
this.removeClientFromQueue(taskId, this.clientId);
|
|
366
423
|
this.abandonWatcher.emit("abandon", taskId);
|
|
367
424
|
return;
|
|
368
425
|
}
|
|
369
|
-
|
|
370
|
-
if (this.queued(taskId)) {
|
|
371
|
-
this.submitAbandonOp(taskId);
|
|
372
|
-
}
|
|
426
|
+
this.submitAbandonOp(taskId);
|
|
373
427
|
this.abandonWatcher.emit("abandon", taskId);
|
|
374
428
|
}
|
|
375
429
|
/**
|
|
@@ -380,9 +434,7 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
380
434
|
return false;
|
|
381
435
|
}
|
|
382
436
|
const currentAssignee = this.taskQueues.get(taskId)?.[0];
|
|
383
|
-
return
|
|
384
|
-
currentAssignee === this.clientId &&
|
|
385
|
-
!this.latestPendingOps.has(taskId));
|
|
437
|
+
return currentAssignee !== undefined && currentAssignee === this.clientId;
|
|
386
438
|
}
|
|
387
439
|
/**
|
|
388
440
|
* {@inheritDoc ITaskManager.queued}
|
|
@@ -391,12 +443,8 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
391
443
|
if (this.isAttached() && !this.connected) {
|
|
392
444
|
return false;
|
|
393
445
|
}
|
|
394
|
-
(0,
|
|
395
|
-
|
|
396
|
-
// If we have no queue for the taskId, then no one has signed up for it.
|
|
397
|
-
return (((clientQueue?.includes(this.clientId) ?? false) &&
|
|
398
|
-
!this.latestPendingOps.has(taskId)) ||
|
|
399
|
-
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;
|
|
400
448
|
}
|
|
401
449
|
/**
|
|
402
450
|
* {@inheritDoc ITaskManager.subscribed}
|
|
@@ -413,15 +461,16 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
413
461
|
}
|
|
414
462
|
// If we are detached we will simulate auto-ack for the complete op. Therefore we only need to send the op if
|
|
415
463
|
// we are attached. Additionally, we don't need to check if we are connected while detached.
|
|
416
|
-
if (this.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
464
|
+
if (this.isDetached()) {
|
|
465
|
+
this.taskQueues.delete(taskId);
|
|
466
|
+
this.completedWatcher.emit("completed", taskId);
|
|
467
|
+
this.emit("completed", taskId);
|
|
468
|
+
return;
|
|
421
469
|
}
|
|
422
|
-
this.
|
|
423
|
-
|
|
424
|
-
|
|
470
|
+
if (!this.connected) {
|
|
471
|
+
throw new Error("Attempted to complete task in disconnected state");
|
|
472
|
+
}
|
|
473
|
+
this.submitCompleteOp(taskId);
|
|
425
474
|
}
|
|
426
475
|
/**
|
|
427
476
|
* {@inheritDoc ITaskManager.canVolunteer}
|
|
@@ -458,13 +507,13 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
458
507
|
}
|
|
459
508
|
}
|
|
460
509
|
const content = [...filteredMap.entries()];
|
|
461
|
-
return (0,
|
|
510
|
+
return (0, internal_5.createSingleBlobSummary)(snapshotFileName, JSON.stringify(content));
|
|
462
511
|
}
|
|
463
512
|
/**
|
|
464
513
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
465
514
|
*/
|
|
466
515
|
async loadCore(storage) {
|
|
467
|
-
const content = await (0,
|
|
516
|
+
const content = await (0, internal_4.readAndParse)(storage, snapshotFileName);
|
|
468
517
|
for (const [taskId, clientIdQueue] of content) {
|
|
469
518
|
this.taskQueues.set(taskId, clientIdQueue);
|
|
470
519
|
}
|
|
@@ -484,12 +533,22 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
484
533
|
onConnect() {
|
|
485
534
|
this.connectionWatcher.emit("connect");
|
|
486
535
|
}
|
|
487
|
-
//
|
|
488
536
|
/**
|
|
489
537
|
* Override resubmit core to avoid resubmission on reconnect. On disconnect we accept our removal from the
|
|
490
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.
|
|
491
540
|
*/
|
|
492
|
-
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
|
+
}
|
|
493
552
|
/**
|
|
494
553
|
* Process a task manager operation
|
|
495
554
|
*
|
|
@@ -500,7 +559,7 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
500
559
|
*/
|
|
501
560
|
processCore(message, local, localOpMetadata) {
|
|
502
561
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
503
|
-
if (message.type ===
|
|
562
|
+
if (message.type === internal_3.MessageType.Operation) {
|
|
504
563
|
const op = message.contents;
|
|
505
564
|
const messageId = localOpMetadata;
|
|
506
565
|
switch (op.type) {
|
|
@@ -523,11 +582,6 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
523
582
|
}
|
|
524
583
|
}
|
|
525
584
|
addClientToQueue(taskId, clientId) {
|
|
526
|
-
const pendingIds = this.pendingCompletedTasks.get(taskId);
|
|
527
|
-
if (pendingIds !== undefined && pendingIds.length > 0) {
|
|
528
|
-
// Ignore the volunteer op if we know this task is about to be completed
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
585
|
// Ensure that the clientId exists in the quorum, or it is placeholderClientId (detached scenario)
|
|
532
586
|
if (this.runtime.getQuorum().getMembers().has(clientId) ||
|
|
533
587
|
this.clientId === placeholderClientId) {
|
|
@@ -537,6 +591,12 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
537
591
|
clientQueue = [];
|
|
538
592
|
this.taskQueues.set(taskId, clientQueue);
|
|
539
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
|
+
}
|
|
540
600
|
const oldLockHolder = clientQueue[0];
|
|
541
601
|
clientQueue.push(clientId);
|
|
542
602
|
const newLockHolder = clientQueue[0];
|
|
@@ -574,11 +634,17 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
574
634
|
* transitioning from detached to attached and this.runtime.clientId is defined.
|
|
575
635
|
*/
|
|
576
636
|
replacePlaceholderInAllQueues() {
|
|
577
|
-
(0,
|
|
637
|
+
(0, internal_2.assert)(this.runtime.clientId !== undefined, 0x475 /* this.runtime.clientId should be defined */);
|
|
578
638
|
for (const clientQueue of this.taskQueues.values()) {
|
|
579
639
|
const clientIdIndex = clientQueue.indexOf(placeholderClientId);
|
|
580
640
|
if (clientIdIndex !== -1) {
|
|
581
|
-
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
|
+
}
|
|
582
648
|
}
|
|
583
649
|
}
|
|
584
650
|
}
|
|
@@ -599,25 +665,55 @@ class TaskManagerClass extends internal_4.SharedObject {
|
|
|
599
665
|
}
|
|
600
666
|
}
|
|
601
667
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
case "complete": {
|
|
610
|
-
this.complete(taskOp.taskId);
|
|
611
|
-
break;
|
|
612
|
-
}
|
|
613
|
-
case "volunteer": {
|
|
614
|
-
this.subscribeToTask(taskOp.taskId);
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
default: {
|
|
618
|
-
(0, internal_1.unreachableCase)(taskOp);
|
|
619
|
-
}
|
|
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;
|
|
620
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);
|
|
621
717
|
}
|
|
622
718
|
}
|
|
623
719
|
exports.TaskManagerClass = TaskManagerClass;
|