Skip to content

Closes #145 - #209

Open
lcalisto wants to merge 5 commits into
NaturalGIS:mainfrom
lcalisto:145-preview-generation
Open

lcalisto wants to merge 5 commits into
NaturalGIS:mainfrom
lcalisto:145-preview-generation

Conversation

@lcalisto

@lcalisto lcalisto commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #145

First version of automatic preview generation, hooked into discovery

  • New tasks/derivers/ package (mirrors tasks/extractors/) renders a raster into a small WEBP (warped to 4326, percentile stretch, nodata as transparency), stored on the record as a derived asset (asset_type={THUMBNAIL,PREVIEW}, bytes in data, bounds in geog, no relative_path) via the DerivedRecordAssetCreate schema from Closes #175 #192

  • Discovery enqueues generation on a dedicated previews dramatiq queue (new compose worker; the existing one is now pinned to default). Enqueue is missing-only, the task is replace-all under a per-record advisory lock, so generation is idempotent and re-running discovery is also the recovery path

  • Eligibility = prefix match against config/preview-folders.json ( real folder list from IPMA) .

This v1 covers rasters that declare their own CRS (.tif/.tiff). Still to be done next: viewer that actually shows these previews (#154), then more formats as separate issues: XYZ via the mission's implicit_crs, vector via Martin ??, KMALL, and SEG-Y (just an icon, still to be decided).

Compose stacks need to gain a previews-worker service.

Note: the issue text says derived datasets are not to be generated automatically. In the July meeting we decided generation is automatic, hooked into discovery so users dont need to manage it. The task is trigger agnostic (idempotent), so a manual button can still be added later if a use case appears.

@lcalisto lcalisto self-assigned this Sep 10, 2026
@lcalisto
lcalisto marked this pull request as ready for review September 12, 2026 18:55
@lcalisto lcalisto mentioned this pull request Sep 24, 2026

@ricardogsilva ricardogsilva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a very commendable effort.

Unfortunately, it seems based on a different assumption than the one that drove the original design - this is maybe my fault for not having made it clearer. I apologize for that 🥲

Letting the generation of derived assets run together with the discovery was meant to mean that the discovery runs first and then it automatically triggers an event to kickstart the derivation - it was not meant to mean that derivation would run intertwined with the discovery. Having discovery and derivation being two independent processes means that we can call any of them in isolation, if needed.

I'm requesting a change, but I'd also be open to changing my mind, if you can convince me that this is a better way than the original plan 😉 - let me know your thoughts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd have this configuration moved to the asset discovery configuration model. Not necessarily on this PR, but we will need to do it at some point.

Also a very minor nitpick - I would prefer the term 'directory' instead of 'folder', in order to keep it homogeneous with the rest of the code. The term folder is more of a GUI-related metaphor, and seems less appropriate here - I don't mean that this needs to be changed for this PR to be merged, it is just a minor note

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Renamed to directory. Moving it into the asset discovery configuration model. Fully agree, issue #213

Comment thread docker/compose.ci.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no bind volume for the config directory here - does this mean we have nothing in CI that needs it? Would it also mean that we don't need this additional previews-worker service?

If we don't need this service in CI, perhaps we can not define it, in order to conserve resources?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right on both the worker is gone from the CI compose. The config bind is not needed there , the file goes inside the image.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no definition of the volume bind for config here either. In this case it seems this would be required for the production deployment to work appropriately.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not needed. the Dockerfile copies the whole thing, prod reads the copied file. It can still be overridden per environment via SEIS_LAB_DATA__PREVIEW_DIRECTORIES_PATH.

Comment thread src/seis_lab_data/cliapp/main.py Outdated
Comment on lines +64 to +65
# extra args are passed straight to dramatiq, which is how a worker gets
# pinned to a queue (`run-processing-worker --queues previews`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be part of the docstring.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done!

Comment on lines +44 to +52
Two discovery runs may ask for previews of the same record at the same time,
and interleaved deletes and inserts would then commit duplicates, so the
replacement is serialized per record with an advisory lock and done in a
single transaction.
"""
await session.execute(
text("SELECT pg_advisory_xact_lock(hashtext(:record_id))"),
{"record_id": str(survey_related_record.id)},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure we need to reach for pg's explicit locking mechanism.

We already have the constants.SurveyRelatedRecordStatus.UNDER_DERIVATION status, which can be set on the record - This is supposed to prevent two simultaneous derivation operations on the same record.

I imagine that the derivation process would start by checking the record's status property and refuse to proceed unless it had a value of either DRAFT or PUBLISHED.

Maybe you looked into this and have a different opinion though?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Lock removed. The flip is a single conditional UPDATE, held only around the write of the derived assets, so a worker killed mid-render cannot strand a record in UNDER_DERIVATION.

Comment on lines +340 to +345
# they become stale as soon as those change; the advisory lock keeps an
# in-flight preview task from re-inserting them after this delete
await session.execute(
text("SELECT pg_advisory_xact_lock(hashtext(:record_id))"),
{"record_id": str(survey_related_record.id)},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As mentioned above, I'd prefer to not resort to using pg explicit locks unless absolutely necessary.

I'm thinking that the operations layer, (which is where most coordination logic should be living) needs to check the current status of a record before calling the db command. If a record is not currently in either DRAFT or PUBLISHED state, then it would simply not be possible to have it be updated. This was the original plan anyway.

The original idea was to have asset derivation be an additional task, after discovery had already been done. We may need to rethink this now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed as well ,the update operation now refuses records that are not DRAFT or PUBLISHED

Comment on lines +442 to +461
# re-running discovery is the recovery path for records whose
# previews are missing, so only those get enqueued again
if deriver_dispatch.is_previewable(
str(found_path), relative_file_path, preview_folders
):
existing_record_id = identifiers.SurveyRelatedRecordId(
existing_asset.survey_related_record_id
)
has_derived_assets = any(
constants.AssetType.DATA not in asset.asset_type
for asset in await asset_queries.collect_all_record_assets(
session, existing_record_id
)
)
if not has_derived_assets:
preview_tasks.generate_record_previews.send(
raw_request_id=str(request_id),
raw_survey_related_record_id=str(existing_record_id),
raw_initiator=json.dumps(dataclasses.asdict(user)),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The original plan was to have discovery decoupled from aux asset derivation, as these are two conceptually different operations. Even if we may perform derivation automatically, after discovery, like you mention in the PR description, the two should still be different and individually addressable operations.

I imagine that after discovery is finished there would be something like a DISCOVERY_FINISHED event which is sent and which would trigger a handler process to then start the derivation process.

@lcalisto lcalisto Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reworked as sugested. discovery's final step enqueues one mission-level derivation task, and the derivation operation is independently callable.

return frozenset(json.loads(contents)["folders"])


async def generate_record_previews(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As mentioned before, this operation needs to check the record's current status in order to know whether to allow mutation of the record

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done! operation skips records that are not DRAFT or PUBLISHED

@lcalisto

lcalisto commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

This is a very commendable effort.

Unfortunately, it seems based on a different assumption than the one that drove the original design - this is maybe my fault for not having made it clearer. I apologize for that 🥲

Letting the generation of derived assets run together with the discovery was meant to mean that the discovery runs first and then it automatically triggers an event to kickstart the derivation - it was not meant to mean that derivation would run intertwined with the discovery. Having discovery and derivation being two independent processes means that we can call any of them in isolation, if needed.

I'm requesting a change, but I'd also be open to changing my mind, if you can convince me that this is a better way than the original plan 😉 - let me know your thoughts

Thanks for the great review @ricardogsilva ! Agree with most of what you raised and no need to apologize, I'm really happy with what we are achieving!

  • Discovery and derivation are now two independent operations, the discovery operation's final step just enqueues one mission-level derivation task, and the derivation operation is callable on its own (which also leaves room for a manual "(re)generate" trigger later, if we ever want one). No event-handler infrastructure for now, let me know if you had something more elaborate in mind.

  • The pg advisory locks are gone. Coordination now uses UNDER_DERIVATION in the operations layer, with one important detail : the status flip is a single conditional update (UPDATE ... SET status = 'UNDER_DERIVATION' WHERE id = ... AND status = <status read at fetch>) held only around the write of the derived assets, not around the whole rendering, a worker killed mid-render (we use max_retries=0) can then never leave a record stranded in UNDER_DERIVATION, and update operations refuse records that are not DRAFT/PUBLISHED.

  • previews-worker removed from the CI compose (nothing consumes the queue there, the integration tests run on the stub broker) .

  • About the config binds: the file ships inside the image (the Dockerfile copies the whole tree), so CI and prod read the inside copy, only dev needs the bind.

  • Moving this configuration into the asset discovery configuration model: agreed, tracked in Move preview configuration into the asset discovery configuration model #213.

@lcalisto
lcalisto requested a review from ricardogsilva October 1, 2026 23:04

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prototype generation of a derived dataset from an already-discovered survey-related record.

2 participants