Skip to content

Commit aa40fc8

Browse files
joaoh82claude
andauthored
SQLR-43 — Go edge/IoT event collector (concurrent writes showcase) (#150)
A Go HTTP collector that buffers telemetry from many concurrent producers into a local .sqlrite file while a background uploader goroutine drains it to a pluggable sink — both writing the same database via BEGIN CONCURRENT (Phase 11 MVCC). The fifth and final app under the SQLR-38 example-apps umbrella. What it exercises: - Go SDK (cgo via sqlrite-ffi) + the process-level sibling-handle registry, so the HTTP path and the uploader each hold their own BEGIN CONCURRENT transaction against one engine. - Row-level conflict detection + the canonical retry-on-ErrBusy loop. - A durable-buffer-for-unreliable-networks shape: survives reboots, stays queryable on-device, optional read-only sqlrite-mcp sidecar. Honest, measured framing: on the v0 engine, BEGIN CONCURRENT is ~0.9x the throughput of a single mutex and ~2x worse tail latency (global engine mutex + per-tx table clone + per-commit O(N) B-tree rebuild — all documented v0 limitations). The win demonstrated here is the capability + correctness (independent in-process writers, zero dropped events under load), not raw speed. The README ships all three measured tables (throughput / latency-under-contention / disjoint-row) and explains why, with reproduce commands. Designs around the verified v0 engine sharp edges, each found by testing: no Go-SDK param binding (inlined+escaped SQL via a single chokepoint); CREATE TABLE IF NOT EXISTS not honored + sqlrite_master not queryable (SELECT-probe for fresh-vs-reopen); CREATE INDEX rejected under MVCC (DDL before the journal_mode switch); the 4 KiB MVCC commit-record cap (payloads bounded at ingest so any single row commits, plus an adaptive checkpoint writer that halves the chunk on a cap error down to one-per-commit); and AUTOINCREMENT collisions under MVCC (app-assigned ids seeded off MAX(id)). Includes: backpressure (503) + /healthz + /stats, a load generator that asserts no drops, Dockerfile + compose (with the MCP sidecar profile), a Makefile, unit/integration tests across all packages, and a go-collector CI job (Linux + macOS) against the in-repo engine. Also: examples index + sqlritedb.com Examples card + a cross-link from docs/concurrent-writes.md. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b40dc7 commit aa40fc8

20 files changed

Lines changed: 3081 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,43 @@ jobs:
296296
working-directory: sdk/go
297297
run: go test -v ./...
298298

299+
# ---------------------------------------------------------------------------
300+
# Go edge/IoT collector example (SQLR-43). Builds + tests the example
301+
# app against the in-repo libsqlrite_c, so an engine/SDK API change
302+
# that breaks the example is caught here. Same {ubuntu, macos} matrix
303+
# and cgo wiring as the go-sdk job.
304+
go-collector:
305+
name: go-collector (${{ matrix.os }})
306+
runs-on: ${{ matrix.os }}
307+
strategy:
308+
fail-fast: false
309+
matrix:
310+
os: [ubuntu-latest, macos-latest]
311+
steps:
312+
- uses: actions/checkout@v4
313+
314+
- uses: actions/setup-go@v5
315+
with:
316+
go-version: '1.21'
317+
cache-dependency-path: examples/go-collector/go.mod
318+
319+
- uses: dtolnay/rust-toolchain@stable
320+
- uses: Swatinem/rust-cache@v2
321+
with:
322+
shared-key: go-collector-${{ matrix.os }}
323+
324+
- name: Build libsqlrite_c
325+
# The Go driver's `#cgo LDFLAGS` references target/release.
326+
run: cargo build --release -p sqlrite-ffi
327+
328+
- name: go vet ./...
329+
working-directory: examples/go-collector
330+
run: go vet ./...
331+
332+
- name: go test ./...
333+
working-directory: examples/go-collector
334+
run: go test -v ./...
335+
299336
# ---------------------------------------------------------------------------
300337
# WASM: only the build + size check (no tests — the WASM module's
301338
# behavior is covered by the other SDKs' test suites; WASM just

docs/concurrent-writes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ Decoders accept v1..=v3. A v2 reader on a v3 WAL emits a clean "unsupported WAL
332332
- [`docs/design-decisions.md`](design-decisions.md) §12a–§12h — the design notes accumulated across Phase 11 sub-phases.
333333
- [`docs/roadmap.md`](roadmap.md#phase-11--concurrent-writes-via-mvcc--begin-concurrent-sqlr-22-in-flight--see-concurrent-writes-planmd) — phase-by-phase shipped vs deferred status.
334334
- [`examples/rust/concurrent_writers.rs`](../examples/rust/concurrent_writers.rs) — runnable retry-loop example.
335+
- [`examples/go-collector/`](../examples/go-collector/) — a Go edge/IoT collector that drives `BEGIN CONCURRENT` from two writers (HTTP path + background uploader) against one file, with **measured** throughput/latency numbers that honestly show the v0 cost of the limitations above (per-tx clone + per-commit rebuild). The README is a good worked example of designing an app around the v0 sharp edges (4 KiB commit cap, no param binding, app-assigned ids under MVCC).
335336

336337
External:
337338

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Beyond the per-SDK quick-start tours above, the [SQLR-38 umbrella](../docs/roadm
2626
| Chat with your notes (MCP) | Node.js | Markdown → SQLRite hybrid retrieval, served to Claude Desktop via `sqlrite-mcp --read-only` | [`nodejs-notes/`](nodejs-notes/) |
2727
| Local-first journaling | Tauri 2 + Svelte 5 | Markdown daily notes in one `.sqlrite` file, BM25 full-text search, "ask my journal" natural-language SQL | [`desktop-journal/`](desktop-journal/) |
2828
| Browser SQL playground | WASM | The full engine in WebAssembly — SQL editor, sample datasets, HNSW vector search, all in a browser tab. Live at [sqlritedb.com/playground](https://sqlritedb.com/playground) | [`wasm-playground/`](wasm-playground/) |
29+
| Edge / IoT event collector | Go | HTTP collector + background uploader writing one `.sqlrite` buffer concurrently via `BEGIN CONCURRENT`; durable-buffer-for-unreliable-networks shape, with measured throughput | [`go-collector/`](go-collector/) |
2930

3031
## Running the Rust quickstart
3132

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# The Docker build context is the repo root (see Dockerfile header).
2+
# Keep the context lean: the Rust stage rebuilds target/ anyway, and
3+
# node_modules / git history bloat the upload.
4+
**/target
5+
**/node_modules
6+
.git
7+
**/*.sqlrite
8+
**/*.sqlrite-wal
9+
web/.next
10+
desktop/src-tauri/target

examples/go-collector/Dockerfile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Multi-stage build for the SQLRite Go edge/IoT collector (SQLR-43).
2+
#
3+
# Build context MUST be the repository root, because the Go module
4+
# links the engine's C library (built from `sqlrite-ffi`) and depends
5+
# on `sdk/go` via a replace directive:
6+
#
7+
# docker build -f examples/go-collector/Dockerfile -t sqlrite-collector .
8+
#
9+
# cgo + cross-compilation note: this image is built for the host's
10+
# architecture. cgo binaries can't be cross-compiled with the plain Go
11+
# toolchain, so produce per-arch images with `docker buildx --platform`
12+
# (the Rust and Go stages both honor the target platform). See the
13+
# README's "Build & distribution matrix" section.
14+
15+
# ---- Stage 1: build libsqlrite_c.so from the engine source ----
16+
FROM rust:1-bookworm AS rust
17+
WORKDIR /src
18+
COPY . .
19+
RUN cargo build --release -p sqlrite-ffi
20+
21+
# ---- Stage 2: build the Go binaries against that library ----
22+
FROM golang:1.22-bookworm AS go
23+
ENV CGO_ENABLED=1
24+
WORKDIR /src
25+
# Bring the whole tree over (we need sdk/go, sqlrite-ffi/include, and
26+
# the freshly-built target/release/libsqlrite_c.so from the rust stage).
27+
COPY --from=rust /src /src
28+
WORKDIR /src/examples/go-collector
29+
RUN go build -trimpath -o /out/collector ./cmd/collector \
30+
&& go build -trimpath -o /out/loadgen ./cmd/loadgen
31+
32+
# ---- Stage 3: slim runtime ----
33+
FROM debian:bookworm-slim
34+
RUN apt-get update \
35+
&& apt-get install -y --no-install-recommends ca-certificates \
36+
&& rm -rf /var/lib/apt/lists/* \
37+
&& useradd --create-home --uid 10001 collector
38+
# Install the shared library where the dynamic linker will find it.
39+
# The Go binary was linked with an rpath pointing at the build tree;
40+
# that path is absent here, so cgo falls through to the ldconfig cache.
41+
COPY --from=rust /src/target/release/libsqlrite_c.so /usr/local/lib/
42+
RUN ldconfig
43+
COPY --from=go /out/collector /usr/local/bin/collector
44+
COPY --from=go /out/loadgen /usr/local/bin/loadgen
45+
USER collector
46+
WORKDIR /data
47+
VOLUME ["/data"]
48+
EXPOSE 8080
49+
ENTRYPOINT ["collector"]
50+
CMD ["-db", "/data/events.sqlrite", "-addr", ":8080"]

examples/go-collector/Makefile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Convenience targets for the SQLRite Go edge/IoT collector.
2+
#
3+
# Everything depends on the engine's C library. `make lib` builds it
4+
# from the repo root; the other targets assume it exists.
5+
6+
REPO_ROOT := ../..
7+
8+
.PHONY: lib build test vet run loadtest bench docker clean
9+
10+
## lib: build the SQLRite cdylib (libsqlrite_c) the cgo driver links.
11+
lib:
12+
cd $(REPO_ROOT) && cargo build --release -p sqlrite-ffi
13+
14+
## build: compile the collector + loadgen binaries.
15+
build: lib
16+
go build -o bin/collector ./cmd/collector
17+
go build -o bin/loadgen ./cmd/loadgen
18+
19+
## test: run the Go test suite (needs lib).
20+
test: lib
21+
go test ./... -count=1
22+
23+
## vet: type-check all packages and tests.
24+
vet: lib
25+
go vet ./...
26+
27+
## run: start the collector on :8080 with a local DB file.
28+
run: build
29+
./bin/collector -db events.sqlrite -addr :8080
30+
31+
## loadtest: fire concurrent producers at a running collector.
32+
loadtest: build
33+
./bin/loadgen -target http://localhost:8080 -workers 64 -duration 30s
34+
35+
## bench: in-process throughput matrix (concurrent vs serialized, ±index).
36+
bench: build
37+
./bin/loadgen -bench -workers 32 -duration 30s
38+
39+
## docker: build the container image (context = repo root).
40+
docker:
41+
cd $(REPO_ROOT) && docker build -f examples/go-collector/Dockerfile -t sqlrite-collector .
42+
43+
## clean: remove build artifacts and local DB files.
44+
clean:
45+
rm -rf bin
46+
rm -f events.sqlrite events.sqlrite-wal

0 commit comments

Comments
 (0)