Repository navigation
Bulk import boxes: imported-box tasks only work on their own import's boxes #1858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ec35aa
22e8204
f07515b
f413547
78b5336
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,12 +80,29 @@ public MlServiceJobOutcome process(JSONObject jobData) { | |
| if (jobData == null) { | ||
| return MlServiceJobOutcome.validationError("INVALID_PAYLOAD", "payload is null"); | ||
| } | ||
| if (jobData.optBoolean("deferredMatch", false)) { | ||
| String taskId = jobData.optString("taskId", null); | ||
| boolean deferred = jobData.optBoolean("deferredMatch", false); | ||
| List<String> named = deferred | ||
| ? jsonArrayToStringList(jobData.optJSONArray("annotationIds")) : new ArrayList<String>(); | ||
| // a payload that would be dispatched as detection (mediaAssetId wins below) names no box | ||
| if (!deferred && jobData.has("annotationId") && !jobData.has("mediaAssetId")) | ||
| named.add(jobData.optString("annotationId", null)); | ||
| String mismatch; | ||
| try { | ||
| mismatch = importedBoxJobMismatch(taskId, named); | ||
| } catch (Exception ex) { | ||
| return MlServiceJobOutcome.persistError("PERSIST", "imported-box check failed: " + ex); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This catch covers every job with a taskId, not only imported boxes. I ran a plain task with parameters
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, good catch. Fixed in f413547: an unreadable-parameters task now fails closed only when something persisted says it could be an imported-box child (the marker text, a stored job that names an annotation, or a held imported box). A plain task with |
||
| } | ||
| if (mismatch != null) { | ||
| System.out.println("WARN: MlServiceProcessor.process() dropped a job for task " + taskId + | ||
| ": " + mismatch); | ||
| return MlServiceJobOutcome.stale(mismatch); | ||
| } | ||
| if (deferred) { | ||
| return runDeferredMatch(jobData); | ||
| } | ||
|
|
||
| String taxonomyString = jobData.optString("taxonomyString", null); | ||
| String taskId = jobData.optString("taskId", null); | ||
| String encounterId = jobData.optString("encounterId", null); | ||
|
|
||
| if (jobData.has("mediaAssetId")) { | ||
|
|
@@ -100,6 +117,69 @@ public MlServiceJobOutcome process(JSONObject jobData) { | |
| "neither mediaAssetId nor annotationId in payload"); | ||
| } | ||
|
|
||
| /** | ||
| * Bulk-import boxes (spec 4.4): an imported-box child works on exactly one box, the one named | ||
| * in the job the server stored on it when it created the child, and that box must carry the | ||
| * stamp of the child's import. Returns why a job must be dropped without any write, or null | ||
| * when it may run (always null for a task that is not an imported-box child). Reads only. | ||
| */ | ||
| String importedBoxJobMismatch(String taskId, List<String> annIds) { | ||
| if (taskId == null) return null; | ||
| Shepherd shep = openShepherd("importedBoxJobMismatch"); | ||
| try { | ||
| shep.beginDBTransaction(); | ||
| // strict lookups: only a genuinely absent row reads as absent; a datastore failure | ||
| // propagates, so an outage can never look like "not an imported-box task" | ||
| Task task; | ||
| try { | ||
| task = (Task)shep.getPM().getObjectById( | ||
| shep.getPM().newObjectIdInstance(Task.class, taskId), true); | ||
| } catch (javax.jdo.JDOObjectNotFoundException notFound) { | ||
| return null; | ||
| } | ||
| if (task.hasUnreadableParameters()) { | ||
| // fail closed when anything persisted says this could be an imported-box child; | ||
| // any other task with unreadable parameters is not one, and its job runs as before | ||
| if (mayBeImportedBoxChild(task)) | ||
| throw new IllegalStateException("task " + taskId + " has unreadable parameters"); | ||
| return null; | ||
| } | ||
| if (!Task.isImportedBoxTask(task)) return null; | ||
| String boxId = null; | ||
| String stored = task.getQueueResumeMessage(); | ||
| if (stored != null) boxId = new JSONObject(stored).optString("annotationId", null); | ||
| if (boxId == null) return "imported-box task has no stored box"; | ||
| if ((annIds == null) || (annIds.size() != 1) || !boxId.equals(annIds.get(0))) | ||
| return "the job does not name this task's box"; | ||
| Annotation ann = IA.strictAnnotation(shep, boxId); // null: deleted, handled downstream | ||
| if ((ann != null) && | ||
| !ann.isImportedBoxOf(task.getParameters().optString("importTaskId", null))) | ||
| return "the annotation is not a box of this task's import"; | ||
| return null; | ||
| } finally { | ||
| shep.rollbackAndClose(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * For a task whose parameters cannot be read: true when other persisted evidence could make it | ||
| * an imported-box child, namely the marker text, a stored job that names an annotation (only | ||
| * extraction children store one), or a held annotation that is an imported box. | ||
| */ | ||
| static boolean mayBeImportedBoxChild(Task task) { | ||
| if (task.rawParametersMentionImportedBox()) return true; | ||
| String stored = task.getQueueResumeMessage(); | ||
| if (stored != null) { | ||
| JSONObject job = Util.stringToJSONObject(stored); | ||
| if ((job == null) || job.has("annotationId")) return true; // unreadable job: fail closed | ||
| } | ||
| List<Annotation> held = task.getObjectAnnotations(); | ||
| if (held != null) { | ||
| for (Annotation ann : held) if ((ann != null) && ann.isImportedBox()) return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private MlServiceJobOutcome processDetection(JSONObject jobData, String taxonomyString, | ||
| String taskId, String encounterId, String maId) { | ||
| DetectionContext det = null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When every box gets skipped here, nothing marks the root, so a client polling it sees a task that never finishes.
rootWithoutAnImportStampExtractsNothingchecks that nothing ran but not the root's status. Maybe mark the root as an invalid envelope when boxes were named and none were taken?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in f413547. When no named box is taken, the root now ends:
error/INVALID_ENVELOPEif a named annotation exists but isn't a box of this import,completedif they were all deleted. A failed commit of that end goes through the existing staging recovery (root endsEXTRACTION_PERSIST).rootWithoutAnImportStampExtractsNothingnow checks the root status; new testsrootWhoseBoxesAreAllGoneCompletesWithNothingToDoandaFailedCommitOfTheEmptyRootEndsItThroughRecovery. The import page's summary does not yet read this request's end for the all-deleted case; that is a follow-up on #1861, which owns the boxed-import completion rule.