Audit #8. `GroupKvServiceImpl` read the group's usage (`count_rows` / `projected_total_bytes`) on one pooled connection and wrote on another. N concurrent writers therefore each observed the same pre-write total, each concluded it had room, and together sailed past the ceiling. The code called this out as "best-effort ... quotas are safety rails, not exact accounting" — but the overshoot is not small. With a ceiling of 5 and 20 concurrent writers, 19 rows land. Route group-KV mutations through `atomic_write::GroupKvWriter`, whose Postgres impl runs the quota reads, the row write, and the shared-trigger fan-out on ONE connection in ONE transaction — and, crucially, takes a per-group `pg_advisory_xact_lock` first. The lock is the fix, not the transaction. Putting the check inside the writing tx is necessary but NOT sufficient: under READ COMMITTED each transaction's `COUNT(*)`/`SUM(...)` still sees a snapshot without the other writers' uncommitted rows, so they all still pass. Serializing the check-then-write per group is what makes a writer see its predecessor's row. The lock key is namespaced per kind (kv/docs/…) so writes drawing on different ceilings don't contend. A delete takes no lock — it only frees space. The quota POLICY (row ceiling, projected-bytes ceiling, and the upper-bound fast path that skips the O(n) SUM scan for a group nowhere near its cap) moves to one place, `group_quota::check_group_write`, over a small `GroupUsage` trait. Both backends — the transactional one reading through `&mut *tx`, and the in-memory one the unit tests use — share it, so there is exactly one copy of the decision. `set_if` gains a correctness nicety on the way: the row ceiling now keys on whether the write ADDS a row, so a compare-and-swap against an absent key with a `Some` precondition (which cannot swap, and so consumes nothing) is no longer charged for one. tests/atomic_write.rs pins both ceilings against 20/16 concurrent writers. Both tests fail without the lock (19 rows stored against a ceiling of 5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PiCloud
A lightweight, self-hosted, event-driven serverless compute platform. Upload a Rhai script, get an HTTP endpoint. Designed to run on a single modest server with no idle CPU cost, and to scale out to a small cluster when you need it.
Status: Phase 1 — MVP scaffolding in progress.
The authoritative design lives in
serverless_cloud_blueprint.md.
Why
Existing serverless platforms are either cloud-locked, heavyweight, or both. PiCloud aims for the opposite end of the spectrum: one binary, one database, one reverse proxy — running on hardware you already own.
Architecture (one paragraph)
PiCloud splits into three logical services — manager (control plane: scripts, schedules, dashboard), orchestrator (per-node event ingress and dispatch), and executor (per-node Rhai sandbox) — each backed by a *-core Rust library. In MVP they run in a single process; in cluster mode they run as three binaries with one manager and one orchestrator + executor per node. Caddy fronts everything; PostgreSQL is the single source of truth.
See CLAUDE.md for working notes and serverless_cloud_blueprint.md for the full design.
Quick Start
Coming as scaffolding lands. For now:
# Rust toolchain (pinned via rust-toolchain.toml)
cargo check --workspace
# Run the all-in-one MVP binary (once main.rs is wired up)
cargo run -p picloud
Repository Layout
crates/
shared/ cross-cutting types
executor-core/ Rhai engine + sandbox
orchestrator-core/ event ingress, dispatch
manager-core/ control plane
picloud/ MVP all-in-one binary
picloud-{manager,orchestrator,executor}/ cluster-mode binaries (skeleton)
dashboard/ SvelteKit
caddy/ Caddyfile
docker/ Dockerfiles
docs/
git-workflow.md Trunk-based workflow
Contributing
See docs/git-workflow.md for the branching and commit conventions. TL;DR: trunk-based, short-lived branches, Conventional Commits, no force-pushing main.
License
TBD.