Repository navigation
OpenConceptLab/ocl_online#655 | Isolated source versions - #985
snyaggarwal wants to merge 6 commits into
Conversation
…eplaces HEAD content, metadata and hierarchy
…ersion tests so they need no Redis
snyaggarwal
left a comment
There was a problem hiding this comment.
Findings
- [P1] Failure atomicity: hierarchy and HEAD membership can change before version persistence completes, so a failed version write can leave partially applied data. The parallel import path also reconciles hierarchy after rejecting a version line. Specific locations and remediation are on the linked tracker issue: OpenConceptLab/ocl_online#655.
- [P2] Mode selection: the workflow path and the persisted mode can diverge when the request flag and Source Version line disagree. Details and expected behavior are on OpenConceptLab/ocl_online#655.
- [P3] OOP / duplicated logic: isolated-version orchestration is implemented separately in the inline, parallel JSON-lines, and archive subtask paths. Consolidating validation, hierarchy reconciliation, and sweep coordination into a shared importer service would prevent their current behavior from drifting; see OpenConceptLab/ocl_online#655 for the concrete differences.
I found no additional P4/P5 findings or nits.
…ommit for every import path; versioning mode resolved before import
|
Addressed in 308432d:
pylint is clean. The isolated-version tests pass with Redis unreachable, and the focused suite has only the |
snyaggarwal
left a comment
There was a problem hiding this comment.
Second review
The prior hierarchy/sweep/version transaction finding and duplicated orchestration finding are addressed. Remaining/new findings:
- [P1] Post-commit ordering can leave a committed version unseeded. The fallible indexing callback is registered before the version-seeding callback; if indexing raises, later callbacks do not run and the import can report failure after the DB commit.
- [P1] A rejected final version still leaves earlier content imports on HEAD. The shared transaction starts after concept/mapping tasks have committed, and the existing-version test only reimports unchanged content.
- [P2] Mode resolution still differs from the linked requirement. The request flag is authoritative instead of the Source Version line/HEAD default, and API version creation still ignores the requested mode.
- [P2] Omitted isolated metadata is inherited. The implementation and test retain fields such as
website, while the linked requirement says missing payload metadata is cleared. - [P2] Archive look-ahead scans every string
versioning_modefield and skips non-string values. This can reject a valid archive because of a Source line or defer an invalid Source Version value until after content imports.
Full reproductions and suggested fixes are on OpenConceptLab/ocl_online#655. No additional P3-P5 findings or nits.
… content imports; robust post-commit indexing; archive look-ahead reads only Source Version lines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Second review addressed in a5a1fd7. Details are on OpenConceptLab/ocl_online#655.
|
| self.update_children_counts(sync=True) | ||
|
|
||
| # after a rollback search and counts stay as they were; robust, so a failure here can't stop version seeding | ||
| transaction.on_commit(after_commit, robust=True) |
There was a problem hiding this comment.
[P1] Persist a retry instead of only logging a failed search repair. robust=True fixes the earlier callback-order bug, but it makes a failed reindex/count update best-effort: the database has already committed is_active=False and removed the HEAD membership rows while Elasticsearch and the stored counts can still describe the old HEAD. The later version indexing is partial (membership/latest-release flags) and does not reliably full-index every dropped HEAD document, so a transient ES failure can leave user-visible search/counts stale until a manual rebuild. Register a durable, retryable task after commit carrying the source and dropped IDs (and expose terminal failure), rather than doing the only repair in a log-only callback. The existing ES down test should also assert that the repair is queued/retried, not just that the DB commit survives.
| for parent_uri in accessible_map[child_uri]: | ||
| inverted.setdefault(parent_uri, []).append(child_uri) | ||
| if inverted: | ||
| make_hierarchy(inverted) |
There was a problem hiding this comment.
[P2] Avoid resolving and materializing the full hierarchy three times. This method first resolves every child/parent URI and builds accessible_map plus the inverted copy. make_hierarchy() then resolves the same URI graph again and materializes pairs/existing; replace_versioned_object_hierarchy() resolves it a third time and builds desired/existing. On a release-sized hierarchy this creates several complete graph copies and repeats the DB lookups inside the final commit transaction. It also splits the same hierarchy domain operation between a module-level task and model methods, which is the duplication/drift risk called out in the review focus. Resolve/access-check once into IDs and let one class/service operation apply both latest-version links and HEAD replacement in chunks. A release-scale query/memory regression test would protect this path.
|
|
||
| concepts_cache = self._build_concepts_cache(diff_keys) if include_changelog_enrichment else {} | ||
| mappings_cache = self._build_mappings_cache(diff_keys) if include_changelog_enrichment else {} | ||
| mappings_cache = self._build_mappings_cache(diff_keys) |
There was a problem hiding this comment.
[P2] The mapping side of the changelog is still unbounded and can reintroduce the OOM/N+1 behavior. _build_mappings_cache() materializes every changed mapping plus its related concepts before the concept batching starts, and mapping_diff_index retains references to all of those model objects in per-concept lists. In the changed_mappings_only branch, mapping.from_concept.display_name also runs without prefetched names or parent data, so it can issue several queries for each unchanged from-concept. The concept cache is bounded, but the mapping object graph and query count remain proportional to the entire diff. Batch mapping IDs too, prefetch the parent/names needed for display names per batch, and emit summaries/index entries incrementally instead of retaining all mapping models.
| return results | ||
|
|
||
| def commit_isolated_version(self, version_line): | ||
| results = self.get_previous_results() |
There was a problem hiding this comment.
[P2] Do not aggregate every per-resource Celery result in the version worker. get_previous_results() concatenates every content task's full result list, and this code keeps that list alive while also building the concept/mapping ID sets. For a 445k-row import this deserializes and retains hundreds of thousands of scalar statuses plus all seen arrays in one worker, then duplicates the identifiers into sets precisely at the most memory-intensive commit step. Only a failure count and the two seen sets are needed: reduce one task result at a time and discard it, or preferably make each content subtask return an aggregate {failure_count, seen} rather than one status per resource.
| def commit_isolated_version(self, version_line): | ||
| result = IsolatedVersionCommit( | ||
| version_line, UserProfile.objects.filter(username=self.username).first(), self.update_if_exists, | ||
| hierarchy_map=self.concept_hierarchy_map, seen_concept_ids=self.get_seen_ids('concepts'), |
There was a problem hiding this comment.
[P3] Reduce child-task results once, not three times. get_seen_ids('concepts'), get_seen_ids('mappings'), and get_content_failures_count() each call get_task_results(), so a large parallel import walks every task result three times and creates three temporary result lists (and may repeat result-backend access). Capture the task results once and derive both ID sets and the failure count in a single pass. A shared result-aggregation object/helper could serve this path and the archive path above, removing another duplicated piece of isolated-version orchestration.
snyaggarwal
left a comment
There was a problem hiding this comment.
Third-round review
The earlier rejection/transaction and callback-order findings are addressed in the current head. This pass found one remaining reliability blocker plus four performance/architecture issues in the new large-import paths. Each finding is posted inline with reproduction impact and a concrete remediation.
Summary: 1× P1, 3× P2, 1× P3. No additional P4/P5 findings or nits.
…s for dropped codes; hierarchy resolved once; import results and changelog mappings handled in batches
|
Third review addressed in da8dc58.
|
…epair task test to the drop so it needs no Redis
OpenConceptLab/ocl_online#655
What this does
This adds isolated source versions, for publishers like LOINC who release each version as a complete, standalone artifact. Today every new version carries HEAD forward. An isolated version instead makes HEAD exactly what the release contains, and then snapshots that HEAD.
You start one with
isolated_version=trueonPOST /importers/bulk-import/. The import must contain the content, then exactly one Source Version line. It works with the JSON-lines and zip importers; npm imports are refused.IsolatedVersionCommitdoes the last step for the inline, parallel and zip/tar runners. It replaces HEAD's hierarchy, drops what the import didn't contain and creates the version in one transaction. If the commit is rejected (a validation error, or the hierarchy root would be dropped), the hierarchy and HEAD membership stay as they were. After the commit, a retried task on theindexingqueue (index_dropped_source_children) reindexes the dropped codes and recounts HEAD. Its Task record shows a final failure.isolated_versiondecides the mode. A Source Version line may repeat it inversioning_mode, but it can't contradict it:isolatedwithout the flag, orcarry_forwardwith it, rejects the whole import up front. The zip importer reads the Source Version lines (only those) while it counts resources.parent_concept_urlsand replace the old links, not add to them. A parent change forces a new concept version.ImportHierarchyresolves the links once into ids, then adds the latest-version links and replaces HEAD's links 5,000 children at a time. The parallel runner and themake_hierarchytask use it too.extras,external_id,datatype,sort_weightand the retire fields from the import, not from its previous version.Source.versioning_mode(carry_forward|isolated, defaultcarry_forward) records how each version was made. It is read-only in the API. Migration0047only adds the column.children/andparents/show the requested version's own members.Changelog compare on large diffs
A 2.81 → 2.82 compare ran out of memory, and once it fit in memory, its result (1.4 GB) was over Postgres's 1 GB
jsonblimit.CHANGELOG_ENRICHMENT_MAX_RESOURCES(default 50,000 new, removed or changed resources), a changelog skips names, descriptions andprev_*, and setsmeta.enrichment_skipped.Also fixed
Decimal(ijson), so any concept with a number inextrasfailed to save.Behaviour changes to be aware of
GETon HEAD returns 404 for it while older versions still have it.Testing
core/importers/tests.pyandcore/integration_tests/tests_sources.py:pylint -j0 core/exits 0.core.importers core.sources core.integration_tests.tests_sources core.common core.concepts core.mappingsran 1,224 tests.BulkImportLimitsTest.test_grandfathered_npm_url_is_downloaded_and_handed_over_as_uploadfails, as it does on master. A few tests that share the local ES fail only in parallel runs and pass on their own.Not in this PR
versioning_mode; isolated versions come from imports only, as decided on the ticket.match_algorithms.idare slow: each runs aMAX(mnemonic)scan, 2–6 s each.The frontend part, showing the versioning mode in version details, is in OpenConceptLab/oclweb3 on the branch of the same name.
🤖 Generated with Claude Code