Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions docs/module-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ One module per PR; no logic changes in a move PR.
`from <pkg> import x`, `mock.patch` targets and `monkeypatch` dotted strings.
Rewrite relative TS imports from their old location. Do not rewrite
`app.state.<pkg>` attributes: these are runtime containers, not imports.
Do not rewrite strings that only look like paths: i18n keys
(`<pkg>.audit.token`), permission names and upgrade ids share the prefix.
Durable event keys and handler names are wire names derived from the
import path: a moved durable handler keeps its old name with
`subscribe_durable(..., name=<constant>)`, and a moved durable event class
needs a data migration. `host/tests/test_durable_wire_names.py` freezes both.
3. Move tests to matching areas/features, keep root `conftest.py`, remove test
package initializers and prefix each basename with the module name.
JS tests remain in `tests-js/`.
Expand Down
55 changes: 55 additions & 0 deletions host/tests/test_durable_wire_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Durable event keys and handler names are wire names: freeze them.

Outbox rows are keyed by the event's ``module.qualname`` and the handler's
name (its import path unless ``name=`` is given). Moving either to another
module (docs/module-layout.md) strands every undelivered row: it retries as an
unknown handler, then goes ``dead``. A move must pin the old name with
``subscribe_durable(..., name=...)``; a changed entry here is a data migration,
not a refactor.
"""

from __future__ import annotations

from simple_module_core.discovery import discover_modules
from simple_module_core.events import EventBus
from simple_module_db.tenant_cleanup import register_tenant_cleanup

_TENANT_DELETED = "simple_module_core.tenant_events.TenantDeleted"
_USER_DELETED = "simple_module_core.user_events.UserDeleted"

EXPECTED: dict[str, frozenset[str]] = {
"file_storage.tenant_deletion.TenantStorageCleanupRequested": frozenset(
{"file_storage.tenant_deletion.purge_objects"}
),
_TENANT_DELETED: frozenset(
{
"api_tokens.tenant_cleanup.purge_tenant_data",
"feature_flags.tenant_cleanup.purge_tenant_data",
"file_storage.tenant_deletion.purge_rows",
"simple_module_db.tenant_cleanup.delete_tenant_settings",
"sharing.tenant_cleanup.purge_tenant_data",
}
),
_USER_DELETED: frozenset(
{
"api_tokens.user_cleanup.purge_user_tokens",
"permissions.user_cleanup.purge_user_grants",
"sharing.user_cleanup.purge_user_shares",
"tenants.user_cleanup.purge_user_memberships",
}
),
"tenants.contracts.events.InvitationCreated": frozenset({"tenants.send_invitation_email"}),
}


def _registered() -> dict[str, frozenset[str]]:
bus = EventBus()
register_tenant_cleanup(bus) # framework-owned, registered by the host and workers
for module in discover_modules():
# The worker registers durable handlers the same way, with no app.
module.register_event_handlers(bus, None)
return {key: frozenset(by_name) for key, by_name in bus.durable._subs.items()}


def test_durable_wire_names_are_unchanged() -> None:
assert _registered() == EXPECTED
4 changes: 4 additions & 0 deletions modules/feature_flags/feature_flags/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@
# flags work perfectly well without an audit log installed.
AUDIT_LOG_MODULE = "AuditLog"
AUDIT_LOG_VIEW_URL = "/admin/audit-log/"

# Durable handler names key undelivered outbox rows, so they keep the import
# path the handler had before the module-layout move (renaming strands rows).
TENANT_CLEANUP_HANDLER = "feature_flags.tenant_cleanup.purge_tenant_data"
3 changes: 2 additions & 1 deletion modules/feature_flags/feature_flags/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PERM_FEATURE_FLAGS_VIEW,
PERM_GROUP,
QP_OVERRIDE,
TENANT_CLEANUP_HANDLER,
VIEW_PREFIX,
)

Expand Down Expand Up @@ -74,7 +75,7 @@ def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) ->

from feature_flags.core.tenant_cleanup import purge_tenant_data

bus.subscribe_durable(TenantDeleted, purge_tenant_data)
bus.subscribe_durable(TenantDeleted, purge_tenant_data, name=TENANT_CLEANUP_HANDLER)
if app is not None:

async def forget_tenant(event: TenantDeleted) -> None:
Expand Down
5 changes: 5 additions & 0 deletions modules/sharing/sharing/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@
ERR_BAD_ROLE: Final = "This role cannot be granted on this resource type"
ERR_EXPIRED: Final = "expires_at must be in the future"
ERR_SELF: Final = "You cannot share a resource with yourself"

# Durable handler names key undelivered outbox rows, so they keep the import
# path the handlers had before the module-layout move (renaming strands rows).
TENANT_CLEANUP_HANDLER: Final = "sharing.tenant_cleanup.purge_tenant_data"
USER_CLEANUP_HANDLER: Final = "sharing.user_cleanup.purge_user_shares"
4 changes: 2 additions & 2 deletions modules/sharing/sharing/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) ->

from sharing.core.tenant_cleanup import purge_tenant_data

bus.subscribe_durable(TenantDeleted, purge_tenant_data)
bus.subscribe_durable(TenantDeleted, purge_tenant_data, name=c.TENANT_CLEANUP_HANDLER)

from simple_module_core.user_events import UserDeleted

from sharing.core.user_cleanup import purge_user_shares

bus.subscribe_durable(UserDeleted, purge_user_shares)
bus.subscribe_durable(UserDeleted, purge_user_shares, name=c.USER_CLEANUP_HANDLER)

def register_settings(self, app: FastAPI) -> None:
from sharing.core.services import SharingServices
Expand Down
Loading