Skip to content

Commit fef8705

Browse files
committed
extract save mutex saga into own module
1 parent 0865908 commit fef8705

File tree

5 files changed

+139
-140
lines changed

5 files changed

+139
-140
lines changed

frontend/javascripts/test/sagas/annotation_saga.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "viewer/model/actions/annotation_actions";
1313
import { ensureWkReady } from "viewer/model/sagas/ready_sagas";
1414
import { wkReadyAction } from "viewer/model/actions/actions";
15-
import { acquireAnnotationMutexMaybe } from "viewer/model/sagas/annotation_saga";
15+
import { acquireAnnotationMutexMaybe } from "viewer/model/sagas/saving/save_mutex_saga";
1616

1717
const createInitialState = (
1818
othersMayEdit: boolean,

frontend/javascripts/viewer/model/sagas/annotation_saga.tsx

Lines changed: 6 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
11
import type { EditableAnnotation } from "admin/rest_api";
2-
import { acquireAnnotationMutex, editAnnotation } from "admin/rest_api";
3-
import { Button } from "antd";
2+
import { editAnnotation } from "admin/rest_api";
43
import ErrorHandling from "libs/error_handling";
54
import Toast from "libs/toast";
65
import * as Utils from "libs/utils";
76
import _ from "lodash";
87
import messages from "messages";
9-
import React from "react";
108
import type { ActionPattern } from "redux-saga/effects";
11-
import {
12-
call,
13-
cancel,
14-
cancelled,
15-
delay,
16-
fork,
17-
put,
18-
retry,
19-
take,
20-
takeEvery,
21-
takeLatest,
22-
} from "typed-redux-saga";
23-
import type { APIUserCompact } from "types/api_types";
9+
import { call, delay, put, retry, take, takeLatest } from "typed-redux-saga";
2410
import constants, { MappingStatusEnum } from "viewer/constants";
2511
import { getMappingInfo, is2dDataset } from "viewer/model/accessors/dataset_accessor";
2612
import { getActiveMagIndexForLayer } from "viewer/model/accessors/flycam_accessor";
2713
import type { Action } from "viewer/model/actions/actions";
28-
import {
29-
type EditAnnotationLayerAction,
30-
type SetAnnotationDescriptionAction,
31-
type SetOthersMayEditForAnnotationAction,
32-
setAnnotationAllowUpdateAction,
33-
setBlockedByUserAction,
14+
import type {
15+
EditAnnotationLayerAction,
16+
SetAnnotationDescriptionAction,
3417
} from "viewer/model/actions/annotation_actions";
3518
import { setVersionRestoreVisibilityAction } from "viewer/model/actions/ui_actions";
3619
import type { Saga } from "viewer/model/sagas/effect-generators";
@@ -48,6 +31,7 @@ import { mayEditAnnotationProperties } from "../accessors/annotation_accessor";
4831
import { needsLocalHdf5Mapping } from "../accessors/volumetracing_accessor";
4932
import { pushSaveQueueTransaction } from "../actions/save_actions";
5033
import { ensureWkReady } from "./ready_sagas";
34+
import { acquireAnnotationMutexMaybe } from "./saving/save_mutex_saga";
5135
import { updateAnnotationLayerName, updateMetadataOfAnnotation } from "./volume/update_actions";
5236

5337
/* Note that this must stay in sync with the back-end constant MaxMagForAgglomerateMapping
@@ -237,121 +221,6 @@ export function* watchAnnotationAsync(): Saga<void> {
237221
yield* takeLatest("EDIT_ANNOTATION_LAYER", pushAnnotationLayerUpdateAsync);
238222
}
239223

240-
export function* acquireAnnotationMutexMaybe(): Saga<void> {
241-
yield* call(ensureWkReady);
242-
const allowUpdate = yield* select((state) => state.annotation.restrictions.allowUpdate);
243-
const annotationId = yield* select((storeState) => storeState.annotation.annotationId);
244-
if (!allowUpdate) {
245-
return;
246-
}
247-
const othersMayEdit = yield* select((state) => state.annotation.othersMayEdit);
248-
const activeUser = yield* select((state) => state.activeUser);
249-
const acquireMutexInterval = 1000 * 60;
250-
const RETRY_COUNT = 12;
251-
const MUTEX_NOT_ACQUIRED_KEY = "MutexCouldNotBeAcquired";
252-
const MUTEX_ACQUIRED_KEY = "AnnotationMutexAcquired";
253-
let isInitialRequest = true;
254-
let doesHaveMutexFromBeginning = false;
255-
let doesHaveMutex = false;
256-
let shallTryAcquireMutex = othersMayEdit;
257-
258-
function onMutexStateChanged(canEdit: boolean, blockedByUser: APIUserCompact | null | undefined) {
259-
if (canEdit) {
260-
Toast.close("MutexCouldNotBeAcquired");
261-
if (!isInitialRequest) {
262-
const message = (
263-
<React.Fragment>
264-
{messages["annotation.acquiringMutexSucceeded"]}{" "}
265-
<Button onClick={() => location.reload()}>Reload the annotation</Button>
266-
</React.Fragment>
267-
);
268-
Toast.success(message, { sticky: true, key: MUTEX_ACQUIRED_KEY });
269-
}
270-
} else {
271-
Toast.close(MUTEX_ACQUIRED_KEY);
272-
const message =
273-
blockedByUser != null
274-
? messages["annotation.acquiringMutexFailed"]({
275-
userName: `${blockedByUser.firstName} ${blockedByUser.lastName}`,
276-
})
277-
: messages["annotation.acquiringMutexFailed.noUser"];
278-
Toast.warning(message, { sticky: true, key: MUTEX_NOT_ACQUIRED_KEY });
279-
}
280-
}
281-
282-
function* tryAcquireMutexContinuously(): Saga<void> {
283-
while (shallTryAcquireMutex) {
284-
if (isInitialRequest) {
285-
yield* put(setAnnotationAllowUpdateAction(false));
286-
}
287-
try {
288-
const { canEdit, blockedByUser } = yield* retry(
289-
RETRY_COUNT,
290-
acquireMutexInterval / RETRY_COUNT,
291-
acquireAnnotationMutex,
292-
annotationId,
293-
);
294-
if (isInitialRequest && canEdit) {
295-
doesHaveMutexFromBeginning = true;
296-
// Only set allow update to true in case the first try to get the mutex succeeded.
297-
yield* put(setAnnotationAllowUpdateAction(true));
298-
}
299-
if (!canEdit || !doesHaveMutexFromBeginning) {
300-
// If the mutex could not be acquired anymore or the user does not have it from the beginning, set allow update to false.
301-
doesHaveMutexFromBeginning = false;
302-
yield* put(setAnnotationAllowUpdateAction(false));
303-
}
304-
if (canEdit) {
305-
yield* put(setBlockedByUserAction(activeUser));
306-
} else {
307-
yield* put(setBlockedByUserAction(blockedByUser));
308-
}
309-
if (canEdit !== doesHaveMutex || isInitialRequest) {
310-
doesHaveMutex = canEdit;
311-
onMutexStateChanged(canEdit, blockedByUser);
312-
}
313-
} catch (error) {
314-
if (process.env.IS_TESTING) {
315-
// In unit tests, that explicitly control this generator function,
316-
// the console.error after the next yield won't be printed, because
317-
// test assertions on the yield will already throw.
318-
// Therefore, we also print the error in the test context.
319-
console.error("Error while trying to acquire mutex:", error);
320-
}
321-
const wasCanceled = yield* cancelled();
322-
if (!wasCanceled) {
323-
console.error("Error while trying to acquire mutex.", error);
324-
yield* put(setBlockedByUserAction(undefined));
325-
yield* put(setAnnotationAllowUpdateAction(false));
326-
doesHaveMutexFromBeginning = false;
327-
if (doesHaveMutex || isInitialRequest) {
328-
onMutexStateChanged(false, null);
329-
doesHaveMutex = false;
330-
}
331-
}
332-
}
333-
isInitialRequest = false;
334-
yield* call(delay, acquireMutexInterval);
335-
}
336-
}
337-
let runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
338-
function* reactToOthersMayEditChanges({
339-
othersMayEdit,
340-
}: SetOthersMayEditForAnnotationAction): Saga<void> {
341-
shallTryAcquireMutex = othersMayEdit;
342-
if (shallTryAcquireMutex) {
343-
if (runningTryAcquireMutexContinuouslySaga != null) {
344-
yield* cancel(runningTryAcquireMutexContinuouslySaga);
345-
}
346-
isInitialRequest = true;
347-
runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
348-
} else {
349-
// othersMayEdit was turned off. The user editing it should be able to edit the annotation.
350-
yield* put(setAnnotationAllowUpdateAction(true));
351-
}
352-
}
353-
yield* takeEvery("SET_OTHERS_MAY_EDIT_FOR_ANNOTATION", reactToOthersMayEditChanges);
354-
}
355224
export default [
356225
warnAboutSegmentationZoom,
357226
watchAnnotationAsync,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { acquireAnnotationMutex } from "admin/rest_api";
2+
import { Button } from "antd";
3+
import Toast from "libs/toast";
4+
import messages from "messages";
5+
import React from "react";
6+
import { call, cancel, cancelled, delay, fork, put, retry, takeEvery } from "typed-redux-saga";
7+
import type { APIUserCompact } from "types/api_types";
8+
import {
9+
type SetOthersMayEditForAnnotationAction,
10+
setAnnotationAllowUpdateAction,
11+
setBlockedByUserAction,
12+
} from "viewer/model/actions/annotation_actions";
13+
import type { Saga } from "viewer/model/sagas/effect-generators";
14+
import { select } from "viewer/model/sagas/effect-generators";
15+
import { ensureWkReady } from "../ready_sagas";
16+
17+
export function* acquireAnnotationMutexMaybe(): Saga<void> {
18+
yield* call(ensureWkReady);
19+
const allowUpdate = yield* select((state) => state.annotation.restrictions.allowUpdate);
20+
const annotationId = yield* select((storeState) => storeState.annotation.annotationId);
21+
if (!allowUpdate) {
22+
return;
23+
}
24+
const othersMayEdit = yield* select((state) => state.annotation.othersMayEdit);
25+
const activeUser = yield* select((state) => state.activeUser);
26+
const acquireMutexInterval = 1000 * 60;
27+
const RETRY_COUNT = 12;
28+
const MUTEX_NOT_ACQUIRED_KEY = "MutexCouldNotBeAcquired";
29+
const MUTEX_ACQUIRED_KEY = "AnnotationMutexAcquired";
30+
let isInitialRequest = true;
31+
let doesHaveMutexFromBeginning = false;
32+
let doesHaveMutex = false;
33+
let shallTryAcquireMutex = othersMayEdit;
34+
35+
function onMutexStateChanged(canEdit: boolean, blockedByUser: APIUserCompact | null | undefined) {
36+
if (canEdit) {
37+
Toast.close("MutexCouldNotBeAcquired");
38+
if (!isInitialRequest) {
39+
const message = (
40+
<React.Fragment>
41+
{messages["annotation.acquiringMutexSucceeded"]}" "
42+
<Button onClick={() => location.reload()}>Reload the annotation</Button>
43+
</React.Fragment>
44+
);
45+
Toast.success(message, { sticky: true, key: MUTEX_ACQUIRED_KEY });
46+
}
47+
} else {
48+
Toast.close(MUTEX_ACQUIRED_KEY);
49+
const message =
50+
blockedByUser != null
51+
? messages["annotation.acquiringMutexFailed"]({
52+
userName: `${blockedByUser.firstName} ${blockedByUser.lastName}`,
53+
})
54+
: messages["annotation.acquiringMutexFailed.noUser"];
55+
Toast.warning(message, { sticky: true, key: MUTEX_NOT_ACQUIRED_KEY });
56+
}
57+
}
58+
59+
function* tryAcquireMutexContinuously(): Saga<void> {
60+
while (shallTryAcquireMutex) {
61+
if (isInitialRequest) {
62+
yield* put(setAnnotationAllowUpdateAction(false));
63+
}
64+
try {
65+
const { canEdit, blockedByUser } = yield* retry(
66+
RETRY_COUNT,
67+
acquireMutexInterval / RETRY_COUNT,
68+
acquireAnnotationMutex,
69+
annotationId,
70+
);
71+
if (isInitialRequest && canEdit) {
72+
doesHaveMutexFromBeginning = true;
73+
// Only set allow update to true in case the first try to get the mutex succeeded.
74+
yield* put(setAnnotationAllowUpdateAction(true));
75+
}
76+
if (!canEdit || !doesHaveMutexFromBeginning) {
77+
// If the mutex could not be acquired anymore or the user does not have it from the beginning, set allow update to false.
78+
doesHaveMutexFromBeginning = false;
79+
yield* put(setAnnotationAllowUpdateAction(false));
80+
}
81+
if (canEdit) {
82+
yield* put(setBlockedByUserAction(activeUser));
83+
} else {
84+
yield* put(setBlockedByUserAction(blockedByUser));
85+
}
86+
if (canEdit !== doesHaveMutex || isInitialRequest) {
87+
doesHaveMutex = canEdit;
88+
onMutexStateChanged(canEdit, blockedByUser);
89+
}
90+
} catch (error) {
91+
if (process.env.IS_TESTING) {
92+
// In unit tests, that explicitly control this generator function,
93+
// the console.error after the next yield won't be printed, because
94+
// test assertions on the yield will already throw.
95+
// Therefore, we also print the error in the test context.
96+
console.error("Error while trying to acquire mutex:", error);
97+
}
98+
const wasCanceled = yield* cancelled();
99+
if (!wasCanceled) {
100+
console.error("Error while trying to acquire mutex.", error);
101+
yield* put(setBlockedByUserAction(undefined));
102+
yield* put(setAnnotationAllowUpdateAction(false));
103+
doesHaveMutexFromBeginning = false;
104+
if (doesHaveMutex || isInitialRequest) {
105+
onMutexStateChanged(false, null);
106+
doesHaveMutex = false;
107+
}
108+
}
109+
}
110+
isInitialRequest = false;
111+
yield* call(delay, acquireMutexInterval);
112+
}
113+
}
114+
let runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
115+
function* reactToOthersMayEditChanges({
116+
othersMayEdit,
117+
}: SetOthersMayEditForAnnotationAction): Saga<void> {
118+
shallTryAcquireMutex = othersMayEdit;
119+
if (shallTryAcquireMutex) {
120+
if (runningTryAcquireMutexContinuouslySaga != null) {
121+
yield* cancel(runningTryAcquireMutexContinuouslySaga);
122+
}
123+
isInitialRequest = true;
124+
runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
125+
} else {
126+
// othersMayEdit was turned off. The user editing it should be able to edit the annotation.
127+
yield* put(setAnnotationAllowUpdateAction(true));
128+
}
129+
}
130+
yield* takeEvery("SET_OTHERS_MAY_EDIT_FOR_ANNOTATION", reactToOthersMayEditChanges);
131+
}

frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { sendSaveRequestWithToken } from "admin/rest_api";
77
import Date from "libs/date";
88
import ErrorHandling from "libs/error_handling";
99
import Toast from "libs/toast";
10-
import { sleep } from "libs/utils";
1110
import window, { alert, document, location } from "libs/window";
1211
import memoizeOne from "memoize-one";
1312
import messages from "messages";

frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2-
acquireAnnotationMutex,
32
type NeighborInfo,
3+
acquireAnnotationMutex,
44
getAgglomeratesForSegmentsFromTracingstore,
55
getEdgesForAgglomerateMinCut,
66
getNeighborsForAgglomerateNode,

0 commit comments

Comments
 (0)