Skip to content

v2.0.17

Choose a tag to compare

@berges99 berges99 released this 13 May 08:53
d6d6e26

What's Changed

  • fix(cli/start): register config errdefer before member name dupe (a1fd290)
  • fix(cli/start): initialise member_env_entries before fallible loop (11499f4)
  • fix(cli/start): non-deterministic --port override vs auto-allocation The original single-pass loop interleaved port reservation with filesystem iteration. With `--port bob=4455` and base=4455: - if readdir yields alice first, alice auto-allocates 4455 (it's free); bob's tryReservePort(4455) then fails — pointing at our own reservation, which lsof can't see ("phantom holder"); - if readdir yields bob first, bob takes 4455; alice rolls forward. Works fine. Behavior depends on filesystem iteration order (APFS vs ext4 vs tmpfs) — non-deterministic. Fix - Split discovery from reservation. Phase 1 just walks the workforce dir and accumulates WorkforceMember entries with port=0, reservation=null. Phase 2 calls a new reserveMemberPorts helper that runs three sub-passes: Pass 0: reject `--port A=N --port B=N` upfront with a clear error, before reserving anything. Pass 1: reserve every explicit `--port NAME=PORT` first, regardless of slice order. Pass 2:o-allocate the rest from the rolling cursor; reserveAvailablePort naturally skips ports Pass 1 holds because we keep their listeners open and bind without SO_REUSEADDR. - All member-port error reporting now lives inside the helper, so the call site collapses to `reserveMemberPorts(...) catch return;`. Removes the inline `allocator.free(name); config.deinit(...); return;` boilerplate that was repeated in the discovery loop. Tests - explicit override wins regardless of slice order: runs the helper with members=[alice,bob] and [bob,alice], --port bob=base. Both orderings produce bob=base, alice=base+something. - auto-allocator skips ports held by Pass 1: members=[alice,bob, charlie], --port charlie=base+1. Asserts alice=base, charlie=base+1, bob=base+2. - duplicate explicit ports rejected upfront: returns error.DuplicateExplicitPort, no reservations made. - explicit-port conflict with external holder fails Pass 1: external reservation occupies port, --port alice=that returns error.PortInUse with "already in use" in stderr. 48/48 tests passing. (483ac05)
  • fix(cli/start): partial-transfer leak when moving ParsedEnv into bucket (3a76382)
  • refactor(cli/start): extract appendDupedKv; drop dead env types (29b699a)
  • fix(cli/start): parseStartArgs leaks opts state on .err returns (ecfe732)
  • fix(cli/start): scope bucket routing and parseEnvFile val_owned leak Scope bucket routing - When a workforce member is named "ui" or "api", scope_names contained the string twice. The storage path for --env / --env-file used first-match (always routing `ui:KEY=VAL` to scope_names[0] = service bucket 1), while the read path used last-match (overwriting ui_bucket with the member's slot). Scoped env entries silently vanished — UI service read an empty bucket, the shadowing member read an empty bucket too. - Replace the name-lookup loop with positional assignment via a new ScopeBuckets helper. scope_names is built in a fixed order (ui? → api? → members), so the read side never needs to do a name lookup. Storage side still uses first-match-by-name, so `--env ui:KEY` always means "the UI service" — never a member that happens to be named "ui". - Emit a startup warning when a workforce member shadows a reserved scope, so the unreachability isn't silent. parseEnvFile leak -val_owned = try allocator.dupe(value)\, an OOM from `out.append(...)` left val_owned dangling: the function-level errdefer on `out` only frees entries already in the list, and the existing errdefer only covered key_owned. Added the missing `errdefer allocator.free(val_owned);` between the dupe and the append. Tests - assignScopeBuckets: ui+api+members → distinct positional buckets, plus all has_ui/has_api on/off combinations. - Scope routing regression: members = ["ui", "worker"], asserts the storage bucket for `ui:` and the UI service's read bucket agree (both = 1), and that the shadowing member gets bucket 3. - parseEnvFile leak: FailingAllocator with fail_index=2 forces OOM on the ArrayList grow after both dupes succeed; testing.allocator panics on leaks at teardown, so a clean exit proves val_owned was freed. 39/39 tests passing. (7db0575)
  • feat(cli/start): port reservations + env overrides Port handling - New flags: --ui-port, --api-port, --workforce-port BASE, --port NAME=PORT (per workforce member). - Explicit ports fail fast with an lsof/netstat-based diagnostic ("requested UI port 3737 is already in use (node, pid 12345)"); default ports auto-roll forward until a free one is found. - PortReservation holds the bound listener in the parent process from discovery through dependency install and releases it microseconds before each child binds. Closes a TOCTOU race where two concurrent timbal start invocations both saw the same default port free during the ~10s uv sync window and then collided at spawn time. Environment overrides - --env [SCOPE:]KEY=VALUE and --env-file [SCOPE:]PATH, both repeatable. Auto-loads .env from the project root and per-member workforce//.env. - SCOPE matches ui, api, or a workforce member name; unscoped entries apply globally. - Precedence (low → high): sofbuilt-ins < shell env (with PORT scrubbed) < auto .env (root + per-member) < --env-file < --env < hard runtime (PORT, TIMBAL_START_*, TIMBAL_WORKFORCE). Tests - Renamed test_all.zig → tests.zig to match upstream Zig convention; switched aggregator to `comptime { _ = @import(...); }` and listed every cli source file so latent tests aren't silently skipped. - ~35 unit tests covering port parsing, port reservation semantics, .env file parng, scope resolution, and env precedence layering. Drive-by fixes: graceful early-exits no longer dump a Zig stack trace; tcgetattr() guarded behind isTty(); workforce member name + config allocations now freed via defer so error paths don't leak; --workforce-port 65535 no longer overflows u16. (9f186d8)

Full Changelog: v2.0.16...v2.0.17