Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
b0e9c1d
add explicit release mutex functionality
philippotto Jun 26, 2025
e02d4ce
remove superfluous didShowFailedSimultaneousTracingError logic (unnec…
philippotto Jun 26, 2025
1154a74
acquire mutex in proofreading and release after saving
philippotto Jun 26, 2025
2e8d41a
retry even 409 errors
philippotto Jun 26, 2025
0865908
Revert "retry even 409 errors" because saving should only be done when a
philippotto Jun 26, 2025
fef8705
extract save mutex saga into own module
philippotto Jun 26, 2025
1ed33f1
refactor further
philippotto Jun 26, 2025
b3865fc
disable eager mutex acquisition and also poll for updates when allow …
philippotto Jun 26, 2025
3ecc502
add DISABLE_EAGER_MUTEX_ACQUISITION bool
philippotto Jun 26, 2025
5e96e38
add todo comment
philippotto Jun 26, 2025
0f41c4b
move isMutexAcquired to store
philippotto Jun 27, 2025
33de8ec
refactor mutex acquisition to use less state
philippotto Jun 27, 2025
527f1db
make mutex-acquisition ad-hoc when trying to save
philippotto Jun 27, 2025
9aa961b
in backend, make agglomerateIds and mag optional in proofreading upda…
fm3 Jun 30, 2025
f845a69
implement has newest version mechanism
philippotto Jul 10, 2025
c3cd37c
don't put unused properties into update actions for split and merge a…
philippotto Jul 10, 2025
29a1608
better debugging
philippotto Jul 10, 2025
aa82211
maintain proofreading marker separately from segment position to avoi…
philippotto Jul 12, 2025
4434111
type fixes
philippotto Jul 12, 2025
9acb66e
try to fix 'Duplicate edge in agglomerate graph' error
philippotto Jul 14, 2025
a65323d
repeat local update after merge because the mapping might have been c…
philippotto Jul 14, 2025
824d660
implement BackendMock class to prepare for new test that deals with p…
philippotto Jul 15, 2025
ef785c4
implement agglomerate helper + tests
philippotto Jul 16, 2025
0ec2347
iterate on backend mock that manages versioned agglomerate mapping
philippotto Jul 17, 2025
c0865d3
fix rebase-merge test
philippotto Jul 17, 2025
96d8706
implement rebase-merge-split test
philippotto Jul 18, 2025
7a11456
refactor test
philippotto Jul 18, 2025
af75fb9
fix most old proofreading tests
philippotto Jul 18, 2025
66bf44e
clean up
philippotto Jul 18, 2025
b22737e
fix one test in save saga spec (but the generator style tests are ann…
philippotto Jul 18, 2025
1822d23
split proofreading spec into multiple modules
philippotto Jul 18, 2025
be1bcca
split proofreading specs into three modules
philippotto Jul 18, 2025
81f9497
add some colored logs
philippotto Jul 18, 2025
0370e9e
WIP refactor mutex acquiring
MichaelBuessemeyer Jul 22, 2025
3f66d4e
clean up backend for mutex release
fm3 Jul 23, 2025
4393fad
fix getting annotation mutex
MichaelBuessemeyer Jul 23, 2025
793c15a
refactor allow update, always keep restrictions set by the server
MichaelBuessemeyer Jul 23, 2025
74cdc79
fix EnsureMaySaveNow not getting resolved
MichaelBuessemeyer Jul 24, 2025
75cfde2
WIP add more mutex tests
MichaelBuessemeyer Jul 25, 2025
64f30e2
wip fix on demand mutex fetching test
MichaelBuessemeyer Jul 25, 2025
9336e7e
fix tests
MichaelBuessemeyer Jul 28, 2025
63d9da6
fix tests
MichaelBuessemeyer Jul 29, 2025
fa855ee
test injecting foreign split action
MichaelBuessemeyer Jul 29, 2025
fe491f9
add segment not loaded in tests
MichaelBuessemeyer Jul 30, 2025
26ca604
added tests where the injected actions include not loaded segments in…
MichaelBuessemeyer Jul 30, 2025
a0fad85
WIP: properly fetch mutex on demand
MichaelBuessemeyer Jul 30, 2025
fb79238
fix on demand mutex releasing and acquiring
MichaelBuessemeyer Jul 31, 2025
0c6d932
mark mutex as released in client state after successful release call …
MichaelBuessemeyer Jul 31, 2025
bfefbc6
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Aug 25, 2025
85cc963
WIP: implement rebase mechanism
MichaelBuessemeyer Aug 26, 2025
c1b71f9
WIP: fix rebase mechanism
MichaelBuessemeyer Aug 26, 2025
5135fe9
Fix multi user proofreading tests
MichaelBuessemeyer Aug 27, 2025
f0f5e0c
WIP: incoporate merge without hard refresh
MichaelBuessemeyer Aug 28, 2025
990e940
load missing info for merge loaded from server
MichaelBuessemeyer Aug 28, 2025
c596863
WIP: do not hard refresh when incoporating split edges actions
MichaelBuessemeyer Aug 29, 2025
8f7f327
save mapping refresh if not necessary
MichaelBuessemeyer Aug 29, 2025
3642423
add wkdev flag enabling new live collab for proofreading
MichaelBuessemeyer Sep 1, 2025
44debba
fix proofreading not allowed after initially activating a mapping in …
MichaelBuessemeyer Sep 1, 2025
c4e9f88
fix rebasing again
MichaelBuessemeyer Sep 2, 2025
9b0f5bd
fix tests
MichaelBuessemeyer Sep 2, 2025
55e7472
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Sep 12, 2025
ed696ff
fix update action order applied by polling mechanism
MichaelBuessemeyer Sep 12, 2025
36c7815
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Sep 22, 2025
172b7a2
fix: do not incorporate missing updates if liveCollab flag is turned off
MichaelBuessemeyer Sep 22, 2025
a1803cf
dont show "not having mutex" toast in liveCollab scenario
MichaelBuessemeyer Sep 22, 2025
92b9184
fix some FE tests
MichaelBuessemeyer Sep 22, 2025
7c6c20f
fix missing tests
MichaelBuessemeyer Sep 23, 2025
76209ed
fix linting and formatting
MichaelBuessemeyer Sep 23, 2025
104a248
add test testing split of segments not loaded by client
MichaelBuessemeyer Sep 23, 2025
70124e4
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Sep 23, 2025
9ab5e79
more tests & moe fixes
MichaelBuessemeyer Sep 24, 2025
b976997
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Sep 25, 2025
f368cf3
WIP make code resilient against more edge cases
MichaelBuessemeyer Sep 25, 2025
30f1f6e
make rebasing update mapping with missing mapping info when applying …
MichaelBuessemeyer Sep 26, 2025
6fe3913
add tests simulating split & merge via mesh interaction where unmappe…
MichaelBuessemeyer Sep 26, 2025
578356c
remove scm boy occurrences
MichaelBuessemeyer Sep 26, 2025
d846ca2
wip add agglomerate skeleton interaction tests
MichaelBuessemeyer Sep 26, 2025
6b0c703
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 2, 2025
94cb969
WIP add first agglomerate skeleton test
MichaelBuessemeyer Oct 2, 2025
41787f7
fix backend mock (only bump version once per update action batch)
MichaelBuessemeyer Oct 2, 2025
35e937c
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 7, 2025
28ff196
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 7, 2025
6820868
apply user specific update actions if apply own actions during rebasing
MichaelBuessemeyer Oct 7, 2025
a60ad8b
make diffable map printable
MichaelBuessemeyer Oct 7, 2025
36c286d
fix self directing edge in agglomerate skeleton generating mocking code
MichaelBuessemeyer Oct 7, 2025
353fe30
WIP: fix unnecessary diff of trees due to rebasing & remove server sp…
MichaelBuessemeyer Oct 7, 2025
0d9ca97
only perform rebasing on demand
MichaelBuessemeyer Oct 8, 2025
9d9add9
fix single user proofreading tests
MichaelBuessemeyer Oct 8, 2025
8f4e8fd
do not enforce busy bloking during rebasing in case of proofreading a…
MichaelBuessemeyer Oct 8, 2025
f5388b9
reload agglomerateIds after syncing with backend & saving in all proo…
MichaelBuessemeyer Oct 9, 2025
ce480fa
fix first agglomerate skeleton test
MichaelBuessemeyer Oct 9, 2025
75f3868
Clean up PR a little
MichaelBuessemeyer Oct 9, 2025
06b5847
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 9, 2025
a1fa9ce
add skeleton merge test with interfered update action after loading a…
MichaelBuessemeyer Oct 9, 2025
1865159
add split agglomerate skeleton tests
MichaelBuessemeyer Oct 9, 2025
2c9b00f
add tests testing split via node context menu via skeleton nodes
MichaelBuessemeyer Oct 12, 2025
912754b
WIP: apply feedback
MichaelBuessemeyer Oct 14, 2025
c88fa69
WIP apply pr feedback
MichaelBuessemeyer Oct 15, 2025
39fff92
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 16, 2025
c157cbb
WIP: apply feedback
MichaelBuessemeyer Oct 16, 2025
403b061
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 16, 2025
617fc01
apply various feedback
MichaelBuessemeyer Oct 16, 2025
b1f5918
add first partitioned mincut test
MichaelBuessemeyer Oct 20, 2025
a46dc69
make tryAcquireMutexForSaving saga more resilient against error durin…
MichaelBuessemeyer Oct 20, 2025
8295a12
add test where partitioned min cut is interfered by merge and thus mi…
MichaelBuessemeyer Oct 20, 2025
e1bcaed
run collab tests in enforced liveCollab = true flag env
MichaelBuessemeyer Oct 20, 2025
34b0aa3
fix clearing receivedDataPerSaveRequest in tests
MichaelBuessemeyer Oct 20, 2025
b88f01f
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 20, 2025
e7f78a4
fix volume update action spec
MichaelBuessemeyer Oct 20, 2025
e9eb6d2
add tests checking for avoiding doing a rebase if not needed
MichaelBuessemeyer Oct 21, 2025
83130ff
fix missing poll only tests
MichaelBuessemeyer Oct 21, 2025
1c9808e
fix a lot of tests
MichaelBuessemeyer Oct 21, 2025
93e86a4
fix imports
MichaelBuessemeyer Oct 21, 2025
f42e5db
fix left over broken tests
MichaelBuessemeyer Oct 22, 2025
51f60d7
make biome ignore snapshot files
MichaelBuessemeyer Oct 22, 2025
777f39f
fix typing
MichaelBuessemeyer Oct 22, 2025
368965a
add comment about introduced updateActiveTree frontend only action
MichaelBuessemeyer Oct 22, 2025
2aa2de6
fix cyclic dependencies
MichaelBuessemeyer Oct 23, 2025
8d2161f
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 23, 2025
f6e4b71
fix backend linting
MichaelBuessemeyer Oct 23, 2025
cba1273
fix frontend import sorting
MichaelBuessemeyer Oct 23, 2025
e35c367
fix skeleton tracing proto mocking in skeleton proofreading tests
MichaelBuessemeyer Oct 23, 2025
39c6949
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 27, 2025
d52852b
do not acquire mutex when switching tools or active volume annotation…
MichaelBuessemeyer Oct 27, 2025
e383ba3
load agglomerate skeleton in version matching the current local annot…
MichaelBuessemeyer Oct 27, 2025
108ba2f
fix syntax
MichaelBuessemeyer Oct 27, 2025
632016c
adjust naming of tests related to save mutex acquisition
MichaelBuessemeyer Oct 27, 2025
d52a1a0
fix tests & add test that mutex is not tried to be acquired when swit…
MichaelBuessemeyer Oct 27, 2025
79523ac
add tests that mutex fetching is done appropriately upon active volum…
MichaelBuessemeyer Oct 27, 2025
d6883fc
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Oct 27, 2025
b55f365
choose correct mutex fetching strategy upon volume layer switching & …
MichaelBuessemeyer Oct 28, 2025
5965020
fix tests by setting editRotation of skeletontracing fixture to 0,0,0
MichaelBuessemeyer Oct 28, 2025
a7455da
fix imports
MichaelBuessemeyer Oct 28, 2025
f5b9532
make test more resilient
MichaelBuessemeyer Oct 29, 2025
b51ab0d
merge origin master into this branch
MichaelBuessemeyer Oct 29, 2025
7a966d3
fix merge
MichaelBuessemeyer Oct 29, 2025
68ad88d
fix problems with latest merge
MichaelBuessemeyer Oct 29, 2025
05ac8b2
remove debug logging
MichaelBuessemeyer Oct 30, 2025
142bb9d
fix typo
MichaelBuessemeyer Oct 30, 2025
2f48fc1
remove todo comment, did the checking and no changes noticed
MichaelBuessemeyer Oct 30, 2025
e5b9e05
fi infinite brain loading spinner
MichaelBuessemeyer Oct 30, 2025
89f0dcf
apply feedback; incl. remove accidentally added file & improve comment
MichaelBuessemeyer Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,12 @@ class AnnotationController @Inject()(
}
}

def releaseMutex(id: ObjectId): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
logTime(slackNotificationService.noticeSlowRequest, durationThreshold = 1 second) {
for {
_ <- annotationMutexService.release(id, request.identity._id) ?~> "annotation.mutex.failed"
} yield Ok
}
}

}
13 changes: 13 additions & 0 deletions app/models/annotation/AnnotationMutexService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ class AnnotationMutexService @Inject()(val lifecycle: ApplicationLifecycle,
_ <- annotationMutexDAO.upsertOne(mutex.copy(expiry = Instant.in(defaultExpiryTime)))
} yield MutexResult(canEdit = true, None)

def release(annotationId: ObjectId, userId: ObjectId): Fox[Unit] =
for {
mutex <- annotationMutexDAO.findOne(annotationId).shiftBox
_ <- mutex match {
case Full(mutex) if mutex.userId == userId =>
annotationMutexDAO.deleteOne(annotationId).map(_ => ())
case _ =>
Fox.successful(())
}
} yield ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we’re not using any info from the findOne other than whether to delete, we could merge the two into one query and just DELETE WHERE annotationId = a AND userId = u

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed that now in 3f66d4e – note that I could not test end-to-end since the frontend isn’t ready. Let me know if this doesn’t work as expected :)


def publicWrites(mutexResult: MutexResult): Fox[JsObject] =
for {
userOpt <- Fox.runOptional(mutexResult.blockedByUser)(user => userDAO.findOne(user)(GlobalAccessContext))
Expand Down Expand Up @@ -114,4 +125,6 @@ class AnnotationMutexDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionC
def deleteExpired(): Fox[Int] =
run(q"DELETE FROM webknossos.annotation_mutexes WHERE expiry < NOW()".asUpdate)

def deleteOne(annotationId: ObjectId): Fox[Int] =
run(q"DELETE FROM webknossos.annotation_mutexes WHERE _annotation = $annotationId".asUpdate)
}
1 change: 1 addition & 0 deletions conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ DELETE /annotations/:id
POST /annotations/:id/merge/:mergedTyp/:mergedId controllers.AnnotationController.mergeWithoutType(id: ObjectId, mergedTyp: String, mergedId: ObjectId)
GET /annotations/:id/download controllers.AnnotationIOController.downloadWithoutType(id: ObjectId, version: Option[Long], skipVolumeData: Option[Boolean], volumeDataZipFormat: Option[String])
POST /annotations/:id/acquireMutex controllers.AnnotationController.tryAcquiringAnnotationMutex(id: ObjectId)
DELETE /annotations/:id/mutex controllers.AnnotationController.releaseMutex(id: ObjectId)

GET /annotations/:typ/:id/info controllers.AnnotationController.info(typ: String, id: ObjectId, timestamp: Option[Long])
DELETE /annotations/:typ/:id controllers.AnnotationController.cancel(typ: String, id: ObjectId)
Expand Down
6 changes: 6 additions & 0 deletions frontend/javascripts/admin/rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,12 @@ export async function acquireAnnotationMutex(
return { canEdit, blockedByUser };
}

export async function releaseAnnotationMutex(annotationId: string): Promise<void> {
await Request.receiveJSON(`/api/annotations/${annotationId}/mutex`, {
method: "DELETE",
});
}

export async function getTracingForAnnotationType(
annotation: APIAnnotation,
annotationLayerDescriptor: AnnotationLayerDescriptor,
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/test/sagas/annotation_saga.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "viewer/model/actions/annotation_actions";
import { ensureWkReady } from "viewer/model/sagas/ready_sagas";
import { wkReadyAction } from "viewer/model/actions/actions";
import { acquireAnnotationMutexMaybe } from "viewer/model/sagas/annotation_saga";
import { acquireAnnotationMutexMaybe } from "viewer/model/sagas/saving/save_mutex_saga";

const createInitialState = (
othersMayEdit: boolean,
Expand Down
4 changes: 2 additions & 2 deletions frontend/javascripts/viewer/api/wk_dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type ApiLoader from "./api_loader";
// Can be accessed via window.webknossos.DEV.flags. Only use this
// for debugging or one off scripts.
export const WkDevFlags = {
logActions: false,
logActions: true,
sam: {
useLocalMask: true,
},
Expand All @@ -28,7 +28,7 @@ export const WkDevFlags = {
disableLayerNameSanitization: false,
},
debugging: {
showCurrentVersionInInfoTab: false,
showCurrentVersionInInfoTab: true,
},
meshing: {
marchingCubeSizeInTargetMag: [64, 64, 64] as Vector3,
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/viewer/default_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const defaultState: WebknossosState = {
contributors: [],
othersMayEdit: false,
blockedByUser: null,
isMutexAcquired: false,
annotationLayers: [],
version: 0,
earliestAccessibleVersion: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type EditAnnotationLayerAction = ReturnType<typeof editAnnotationLayerAct
export type SetAnnotationDescriptionAction = ReturnType<typeof setAnnotationDescriptionAction>;
type SetAnnotationAllowUpdateAction = ReturnType<typeof setAnnotationAllowUpdateAction>;
type SetBlockedByUserAction = ReturnType<typeof setBlockedByUserAction>;
export type SetIsMutexAcquiredAction = ReturnType<typeof setIsMutexAcquiredAction>;
type SetUserBoundingBoxesAction = ReturnType<typeof setUserBoundingBoxesAction>;
type FinishedResizingUserBoundingBoxAction = ReturnType<
typeof finishedResizingUserBoundingBoxAction
Expand Down Expand Up @@ -87,6 +88,7 @@ export type AnnotationActionTypes =
| SetAnnotationDescriptionAction
| SetAnnotationAllowUpdateAction
| SetBlockedByUserAction
| SetIsMutexAcquiredAction
| SetUserBoundingBoxesAction
| ChangeUserBoundingBoxAction
| FinishedResizingUserBoundingBoxAction
Expand Down Expand Up @@ -176,6 +178,12 @@ export const setBlockedByUserAction = (blockedByUser: APIUserCompact | null | un
blockedByUser,
}) as const;

export const setIsMutexAcquiredAction = (isMutexAcquired: boolean) =>
({
type: "SET_IS_MUTEX_ACQUIRED",
isMutexAcquired,
}) as const;

// Strictly speaking this is no annotation action but a tracing action, as the boundingBox is saved with
// the tracing, hence no ANNOTATION in the action type.
export const setUserBoundingBoxesAction = (userBoundingBoxes: Array<UserBoundingBox>) =>
Expand Down
24 changes: 23 additions & 1 deletion frontend/javascripts/viewer/model/actions/save_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type DisableSavingAction = ReturnType<typeof disableSavingAction>;
export type EnsureTracingsWereDiffedToSaveQueueAction = ReturnType<
typeof ensureTracingsWereDiffedToSaveQueueAction
>;
export type EnsureMaySaveNowAction = ReturnType<typeof ensureMaySaveNowAction>;
export type DoneSavingAction = ReturnType<typeof doneSavingAction>;

export type SaveAction =
| PushSaveQueueTransaction
Expand All @@ -38,7 +40,9 @@ export type SaveAction =
| UndoAction
| RedoAction
| DisableSavingAction
| EnsureTracingsWereDiffedToSaveQueueAction;
| EnsureTracingsWereDiffedToSaveQueueAction
| EnsureMaySaveNowAction
| DoneSavingAction;

// The action creators pushSaveQueueTransaction and pushSaveQueueTransactionIsolated
// are typed so that update actions that need isolation are isolated in a group each.
Expand Down Expand Up @@ -132,3 +136,21 @@ export const ensureTracingsWereDiffedToSaveQueueAction = (callback: (tracingId:
type: "ENSURE_TRACINGS_WERE_DIFFED_TO_SAVE_QUEUE",
callback,
}) as const;

export const ensureMaySaveNowAction = (callback: () => void) =>
({
type: "ENSURE_MAY_SAVE_NOW",
callback,
}) as const;

export const dispatchEnsureMaySaveNowAsync = async (dispatch: Dispatch<any>): Promise<void> => {
const readyDeferred = new Deferred();
const action = ensureMaySaveNowAction(() => readyDeferred.resolve(null));
dispatch(action);
await readyDeferred.promise();
};

export const doneSavingAction = () =>
({
type: "DONE_SAVING",
}) as const;
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ function AnnotationReducer(state: WebknossosState, action: Action): WebknossosSt
});
}

case "SET_IS_MUTEX_ACQUIRED": {
const { isMutexAcquired } = action;
return updateKey(state, "annotation", {
isMutexAcquired,
});
}

case "SET_USER_BOUNDING_BOXES": {
return updateUserBoundingBoxes(state, action.userBoundingBoxes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function convertServerAnnotationToFrontendAnnotation(
othersMayEdit,
annotationLayers,
blockedByUser: null,
isMutexAcquired: false,
};
}

Expand Down
143 changes: 6 additions & 137 deletions frontend/javascripts/viewer/model/sagas/annotation_saga.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,19 @@
import type { EditableAnnotation } from "admin/rest_api";
import { acquireAnnotationMutex, editAnnotation } from "admin/rest_api";
import { Button } from "antd";
import { editAnnotation } from "admin/rest_api";
import ErrorHandling from "libs/error_handling";
import Toast from "libs/toast";
import * as Utils from "libs/utils";
import _ from "lodash";
import messages from "messages";
import React from "react";
import type { ActionPattern } from "redux-saga/effects";
import {
call,
cancel,
cancelled,
delay,
fork,
put,
retry,
take,
takeEvery,
takeLatest,
} from "typed-redux-saga";
import type { APIUserCompact } from "types/api_types";
import { call, delay, put, retry, take, takeLatest } from "typed-redux-saga";
import constants, { MappingStatusEnum } from "viewer/constants";
import { getMappingInfo, is2dDataset } from "viewer/model/accessors/dataset_accessor";
import { getActiveMagIndexForLayer } from "viewer/model/accessors/flycam_accessor";
import type { Action } from "viewer/model/actions/actions";
import {
type EditAnnotationLayerAction,
type SetAnnotationDescriptionAction,
type SetOthersMayEditForAnnotationAction,
setAnnotationAllowUpdateAction,
setBlockedByUserAction,
import type {
EditAnnotationLayerAction,
SetAnnotationDescriptionAction,
} from "viewer/model/actions/annotation_actions";
import { setVersionRestoreVisibilityAction } from "viewer/model/actions/ui_actions";
import type { Saga } from "viewer/model/sagas/effect-generators";
Expand All @@ -48,6 +31,7 @@ import { mayEditAnnotationProperties } from "../accessors/annotation_accessor";
import { needsLocalHdf5Mapping } from "../accessors/volumetracing_accessor";
import { pushSaveQueueTransaction } from "../actions/save_actions";
import { ensureWkReady } from "./ready_sagas";
import { acquireAnnotationMutexMaybe } from "./saving/save_mutex_saga";
import { updateAnnotationLayerName, updateMetadataOfAnnotation } from "./volume/update_actions";

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

export function* acquireAnnotationMutexMaybe(): Saga<void> {
yield* call(ensureWkReady);
const allowUpdate = yield* select((state) => state.annotation.restrictions.allowUpdate);
const annotationId = yield* select((storeState) => storeState.annotation.annotationId);
if (!allowUpdate) {
return;
}
const othersMayEdit = yield* select((state) => state.annotation.othersMayEdit);
const activeUser = yield* select((state) => state.activeUser);
const acquireMutexInterval = 1000 * 60;
const RETRY_COUNT = 12;
const MUTEX_NOT_ACQUIRED_KEY = "MutexCouldNotBeAcquired";
const MUTEX_ACQUIRED_KEY = "AnnotationMutexAcquired";
let isInitialRequest = true;
let doesHaveMutexFromBeginning = false;
let doesHaveMutex = false;
let shallTryAcquireMutex = othersMayEdit;

function onMutexStateChanged(canEdit: boolean, blockedByUser: APIUserCompact | null | undefined) {
if (canEdit) {
Toast.close("MutexCouldNotBeAcquired");
if (!isInitialRequest) {
const message = (
<React.Fragment>
{messages["annotation.acquiringMutexSucceeded"]}{" "}
<Button onClick={() => location.reload()}>Reload the annotation</Button>
</React.Fragment>
);
Toast.success(message, { sticky: true, key: MUTEX_ACQUIRED_KEY });
}
} else {
Toast.close(MUTEX_ACQUIRED_KEY);
const message =
blockedByUser != null
? messages["annotation.acquiringMutexFailed"]({
userName: `${blockedByUser.firstName} ${blockedByUser.lastName}`,
})
: messages["annotation.acquiringMutexFailed.noUser"];
Toast.warning(message, { sticky: true, key: MUTEX_NOT_ACQUIRED_KEY });
}
}

function* tryAcquireMutexContinuously(): Saga<void> {
while (shallTryAcquireMutex) {
if (isInitialRequest) {
yield* put(setAnnotationAllowUpdateAction(false));
}
try {
const { canEdit, blockedByUser } = yield* retry(
RETRY_COUNT,
acquireMutexInterval / RETRY_COUNT,
acquireAnnotationMutex,
annotationId,
);
if (isInitialRequest && canEdit) {
doesHaveMutexFromBeginning = true;
// Only set allow update to true in case the first try to get the mutex succeeded.
yield* put(setAnnotationAllowUpdateAction(true));
}
if (!canEdit || !doesHaveMutexFromBeginning) {
// If the mutex could not be acquired anymore or the user does not have it from the beginning, set allow update to false.
doesHaveMutexFromBeginning = false;
yield* put(setAnnotationAllowUpdateAction(false));
}
if (canEdit) {
yield* put(setBlockedByUserAction(activeUser));
} else {
yield* put(setBlockedByUserAction(blockedByUser));
}
if (canEdit !== doesHaveMutex || isInitialRequest) {
doesHaveMutex = canEdit;
onMutexStateChanged(canEdit, blockedByUser);
}
} catch (error) {
if (process.env.IS_TESTING) {
// In unit tests, that explicitly control this generator function,
// the console.error after the next yield won't be printed, because
// test assertions on the yield will already throw.
// Therefore, we also print the error in the test context.
console.error("Error while trying to acquire mutex:", error);
}
const wasCanceled = yield* cancelled();
if (!wasCanceled) {
console.error("Error while trying to acquire mutex.", error);
yield* put(setBlockedByUserAction(undefined));
yield* put(setAnnotationAllowUpdateAction(false));
doesHaveMutexFromBeginning = false;
if (doesHaveMutex || isInitialRequest) {
onMutexStateChanged(false, null);
doesHaveMutex = false;
}
}
}
isInitialRequest = false;
yield* call(delay, acquireMutexInterval);
}
}
let runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
function* reactToOthersMayEditChanges({
othersMayEdit,
}: SetOthersMayEditForAnnotationAction): Saga<void> {
shallTryAcquireMutex = othersMayEdit;
if (shallTryAcquireMutex) {
if (runningTryAcquireMutexContinuouslySaga != null) {
yield* cancel(runningTryAcquireMutexContinuouslySaga);
}
isInitialRequest = true;
runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously);
} else {
// othersMayEdit was turned off. The user editing it should be able to edit the annotation.
yield* put(setAnnotationAllowUpdateAction(true));
}
}
yield* takeEvery("SET_OTHERS_MAY_EDIT_FOR_ANNOTATION", reactToOthersMayEditChanges);
}
export default [
warnAboutSegmentationZoom,
watchAnnotationAsync,
Expand Down
Loading
Loading