You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 098a92c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(hosting): wizard migrates the app's database; keep explicit bootstrap fields; one dotenv source; bounded Redis leases (#803)
* fix(hosting): migrate the app's database; keep explicit bootstrap fields; one dotenv source; bounded Redis leases
- setup wizard passes the app engine URL to Alembic (`sm.database_url`
attribute); host and scaffolded env.py prefer it over Settings(), so the
migrated database is the one the lock covers (#782)
- merge_host_settings(bootstrap) preserves every field set on the bootstrap
(#783)
- HostSettings discovers the project .env like BootstrapSettings, so boot and
runtime reads agree without load_dotenv_into_environ() (#793)
- lease acquire bounds each backend call by the remaining wait (1 s floor);
Redis client gets 2 s socket timeouts and no hidden retries; acquire
timeouts/connection failures return False, release failures log (#787)
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* docs(leases): 1 s minimum attempt budget
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* refactor(optimize): fold an import; one-line Alembic URL fallback
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* fix: address code review findings (round 1, pass 1)
Mark the .env precedence and lease acquisition entries as behaviour changes.
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* fix: address code review findings (round 1, pass 2)
- one immediate Redis reconnect so a stale pooled connection is not an outage
- ALEMBIC_DATABASE_URL_ATTRIBUTE constant shared by the wizard and env.py
- leases docs: only timeouts and Redis connection failures return False
- CHANGELOG: upgrade note for hosts scaffolded before #782
- lease tests: tighter limit assertions, looser wall-clock bounds
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* docs(i18n): runtime HostSettings resolution order
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* docs(leases): worst-case Redis call time
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
* test: mark the simulated-latency sleeps as intentional
Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq
Copy file name to clipboardExpand all lines: docs/framework/i18n.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -214,7 +214,7 @@ A pydantic validator enforces that the default locale is in the supported list.
214
214
`HostSettings` is consumed through **two different objects**, and i18n uses the env-backed one:
215
215
216
216
-`Settings` (`HostSettings` + `BootstrapSettings`) inherits `env_prefix="SM_"`, so every field on it resolves from `SM_*` at boot. This instance lands on `app.state.sm.settings`, and it is what `LocaleMiddleware`, the i18n manifest, the shared-props builder and `i18n_deps` all read.
217
-
-`app.state.host.settings` is a separate `HostSettings` hydrated from the DB. `HostSettings` on its own declares no `env_prefix`, so this instance is pydantic defaults plus stored overrides. `maintenance_mode` is read from here, which is what makes that flag live-editable.
217
+
-`app.state.host.settings` is a separate `HostSettings` hydrated from the DB. It resolves env (process variables, then the project `.env`) → stored overrides → defaults, so an admin-UI edit lands here unless env pins the field. `maintenance_mode` is read from here, which is what makes that flag live-editable.
218
218
219
219
The practical consequence: **changing the i18n values in the admin UI does not change locale resolution.**`LocaleMiddleware` captured its locale set at construction from the env-derived object, and a settings save swaps `app.state.host.settings`, not `app.state.sm.settings`. Moving the served locale set means setting the env var and restarting. The same is true of `multi_tenant` / `tenant_header` — `SM_MULTI_TENANT` is what decides whether `TenantMiddleware` is installed at all, and no DB write can install a middleware after boot.
Copy file name to clipboardExpand all lines: docs/framework/leases.md
+12-1Lines changed: 12 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,7 +53,18 @@ The backend is process-global because a Celery worker has no `app.state`. `creat
53
53
| a database, no Redis |`DbLeaseBackend` (`simple_module_db`) | Rows in `sm_lease (name, slot, holder, fence, expires_at)`, primary key `(name, slot)`. Every comparison uses the database clock, so clock skew between hosts does not matter; on Postgres the clock is read after the slot row is locked, so a lock wait never shortens a TTL. |
54
54
| neither |`InProcessLeaseBackend`| One process only. Also what tests and eager Celery get when nothing else is configured. |
55
55
56
-
Backend errors propagate. A lease that the backend could not confirm is never reported as held. This is unlike the rate limiter, which falls back to per-process counters.
56
+
Acquisition timeouts, and Redis connection failures, log a WARNING and return
57
+
`False` (other backends' errors, such as a database outage under
58
+
`DbLeaseBackend`, still propagate); `async with` consequently raises `LeaseNotAcquiredError`. An explicit
59
+
`wait` bounds backend calls as well as polling, with a 1-second minimum attempt
60
+
budget (including `wait=0`). Redis connections and socket reads each have a
61
+
2-second timeout and at most one immediate reconnect (so roughly 4 s at
62
+
worst against an unresponsive server), which also bounds calls when no
63
+
`wait` is supplied. A Redis release timeout or connection failure logs a
64
+
WARNING and returns `False`, so cleanup preserves an exception from the lease
65
+
body; the remote holder expires by TTL. Refresh errors and other backend
66
+
errors still propagate. There is no fallback to a local lease: a lease that
67
+
the backend could not confirm is never reported as held.
57
68
58
69
Override the backend with `configure_leases(backend)`, or pass `backend=` to a single `lease()`/`semaphore()` call. `configure_leases(None)` restores a fresh in-process backend, which is useful in test teardown.
Copy file name to clipboardExpand all lines: docs/framework/settings.md
+15Lines changed: 15 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,6 +29,21 @@ class BootstrapSettings(BaseSettings):
29
29
30
30
The `SM_` prefix means every field is set via `SM_<FIELD>` (e.g. `SM_DATABASE_URL`, `SM_LOG_FORMAT`). `multi_tenant` / `tenant_header` and the other tunables live on the DB-backed `HostSettings` half.
31
31
32
+
Hosts call `merge_host_settings()` before `create_app` to apply stored host
33
+
settings before middleware and module discovery are built. Precedence is
34
+
**env → DB → default**. Here, env includes the discovered project `.env`;
35
+
process environment variables take precedence over that file. Both the boot
36
+
`Settings` and runtime `HostSettings` discover the same file via
37
+
`find_env_file()`, including from a project subdirectory, without requiring
38
+
`load_dotenv_into_environ()` first. Direct settings construction supports an
39
+
explicit `_env_file` path or `_env_file=None` to disable dotenv loading.
40
+
41
+
When passing a `BootstrapSettings` to `merge_host_settings(bootstrap)`, every
42
+
field explicitly supplied on that object is preserved, including `environment`,
43
+
`modules_enabled`, `redis_url`, `debug` and `vite_dev_url`. The resolved secret
44
+
key and database URL are preserved too. Fields the bootstrap did not supply
45
+
still use env → DB → default.
46
+
32
47
Construct once at boot, put on `app.state.sm.settings`, never mutated.
Copy file name to clipboardExpand all lines: docs/reference/env-vars.md
+24-22Lines changed: 24 additions & 22 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,26 +53,27 @@ Pools are **per process**. With multiple `uvicorn --workers`, total connections
53
53
54
54
## Host settings (`HostSettings`)
55
55
56
-
These are declared on `HostSettings` and registered under `package="host"`, so they appear in the admin UI at `/admin/settings/`. They are **also** readable from env: `Settings` combines `HostSettings` with `BootstrapSettings` and inherits its `env_prefix="SM_"`, so each field below resolves from the matching `SM_*` variable at boot.
57
-
58
-
Which source actually wins depends on the field, because `HostSettings` is consumed through two different objects:
59
-
60
-
| Setting | Env var | Default | Read from | Notes |
61
-
|---|---|---|---|---|
62
-
|`multi_tenant`|`SM_MULTI_TENANT`|`false`|**env at boot**| Decides whether `TenantMiddleware` is installed. A DB write cannot install a middleware after boot. |
63
-
|`tenant_header`|`SM_TENANT_HEADER`|`""`|**env at boot**| Header identifying the tenant (empty = header lookup disabled). |
64
-
|`i18n_default_locale`|`SM_I18N_DEFAULT_LOCALE`|`en`|**env at boot**| Must be in `i18n_supported_locales`. |
65
-
|`i18n_supported_locales`|`SM_I18N_SUPPORTED_LOCALES`|`["en"]`|**env at boot**| JSON array, e.g. `["en","es"]`. |
66
-
|`i18n_cookie_name`|`SM_I18N_COOKIE_NAME`|`locale`|**env at boot**| Cookie storing the selected locale. |
67
-
|`maintenance_mode`|*(none — see below)*|`false`|**DB at request time**| Serve everyone but admins a 503. DB-backed so flipping it needs no redeploy — see [Deployment](/reference/deployment#maintenance-mode). |
68
-
|`maintenance_message`|*(none — see below)*|`""`|**DB at request time**| Optional operator note on the maintenance page. |
69
-
|`public_base_url`|`SM_PUBLIC_BASE_URL`|`""`|**DB at request time**| Public origin (`https://example.org`) for canonical / `og:url` in the server-rendered head. Never taken from the request `Host`. See [Inertia](/frontend/inertia#seo-head). |
70
-
71
-
The split is not arbitrary. The boot instance (`Settings()`, env-derived) lands on `app.state.sm.settings` and is what configures middleware at construction — `LocaleMiddleware` captures its locale set there, and the tenancy flag decides whether `TenantMiddleware` is added at all. The DB-hydrated instance is a plain `HostSettings` on `app.state.host.settings`, which declares no `env_prefix` and so is defaults-plus-overrides; `MaintenanceMiddleware` reads that one on every request.
72
-
73
-
So editing the tenancy or i18n rows in the admin UI updates what the UI shows without moving what the app serves — those need the env var and a restart. Maintenance mode is the one that genuinely takes effect live.
74
-
75
-
The two maintenance fields have **no working env var**, and this is worth stating plainly because the shape of the code suggests otherwise. `SM_MAINTENANCE_MODE=true` does set `maintenance_mode` on the boot `Settings()` object — but nothing reads maintenance from there, and the DB-hydrated `HostSettings` that `MaintenanceMiddleware` does read carries no `SM_` prefix, so it never sees the variable. Setting it looks plausible and silently does nothing. Flip the flag in the admin UI, or write the settings row directly.
56
+
These are declared on `HostSettings` and registered under `package="host"`, so
57
+
they appear in the admin UI at `/admin/settings/`. Both boot `Settings` and
58
+
runtime `HostSettings` resolve **env → DB → default**, where env includes the
59
+
discovered project `.env` and process variables take precedence over that file.
60
+
61
+
| Setting | Env var | Default | Applied |
62
+
|---|---|---|---|
63
+
|`multi_tenant`|`SM_MULTI_TENANT`|`false`| At boot; restart required. |
64
+
|`tenant_header`|`SM_TENANT_HEADER`|`""`| At boot; restart required. |
65
+
|`i18n_default_locale`|`SM_I18N_DEFAULT_LOCALE`|`en`| At boot; must be in supported locales. Restart required. |
66
+
|`i18n_supported_locales`|`SM_I18N_SUPPORTED_LOCALES`|`["en"]`| At boot; restart required. |
67
+
|`i18n_cookie_name`|`SM_I18N_COOKIE_NAME`|`locale`| At boot; restart required. |
68
+
|`maintenance_mode`|`SM_MAINTENANCE_MODE`|`false`| Runtime; serve everyone but admins a 503. |
0 commit comments