Skip to content

fix(carts): repair dolt server reuse, auth, and start race - #736

Open
rupakm wants to merge 3 commits into
mainfrom
fix/carts-dolt-server-lifecycle
Open

fix(carts): repair dolt server reuse, auth, and start race#736
rupakm wants to merge 3 commits into
mainfrom
fix/carts-dolt-server-lifecycle

Conversation

@rupakm

@rupakm rupakm commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

ox carts is usable again. Before this, the very first command on a machine failed outright, and once someone got past that, every command after the first failed until they manually killed a stray dolt process:

Error: open carts store: ping dolt server: Error 1045 (28000): Access denied for user 'Ada Lovelace'
Error: open carts store: ensure dolt server: server failed to start: timeout waiting for dolt server on port 58054

Three independent defects in the dolt sql-server lifecycle, each of which alone breaks the command.

Approach: connect as root rather than as the git author name, check liveness with syscall.Signal(0) rather than a nil signal, and record the PID/port only after the readiness probe passes.

1. The SQL user was the git author name

store.go built the DSN from CommitterName — a git author name like Ada Lovelace, not a SQL account. dolt sql-server only ever provisions root, so every command failed with Access denied unless the user happened to be named root. CommitterName is now used solely for the DOLT_COMMIT author, which is what it was for.

2. Server reuse was dead code

runningServerPort probed liveness with proc.Signal(os.Signal(nil)). os.Process.Signal type-asserts its argument to syscall.Signal, and nil fails that assertion, so the call always returned an error. Reuse never once succeeded: every invocation started another dolt sql-server, and all but the first died on dolt's exclusive write lock.

A live PID also isn't proof the server is ours — the OS recycles PIDs — so reuse now additionally confirms something is serving the recorded port.

3. State files were written before the server was known to be up

startServer wrote dolt-server.{pid,port} immediately after cmd.Start(), then waited for readiness. A starter about to lose the write-lock race would overwrite the metadata of the server that won it, orphaning the healthy server: every later invocation read the loser's dead PID and started yet another doomed one. The files are now written after the probe passes, and a failed start reaps the process it spawned instead of leaving it holding the lock.

What changed

File Change
internal/carts/store.go DSN connects as root; CommitterName reserved for the commit author
internal/carts/server.go syscall.Signal(0) liveness check; port-serving confirmation; state files written post-readiness; spawned process reaped on failed start
internal/carts/server_test.go New — regression coverage for the liveness probe and stale-PID handling

Test Plan

  • go test ./internal/carts/ — passes, 5 new cases (3 liveness subtests + 2 runningServerPort cases)
  • go vet ./internal/carts/ and gofmt -l — clean
  • One subtest asserts a nil signal still errors, so reintroducing the original bug fails the suite
  • Manual, against a real carts DB: 4 consecutive ox carts invocations succeed, pgrep dolt sql-server shows exactly one server, and the state files match the live PID/port
  • Manual: ox carts create succeeds and dolt log shows the correct author (Name <email>)
  • Verified the pre-fix binary reproduces all three failures on the same DB
Why this went unnoticed

Each defect masks the others. (1) fires first and stops anyone from reaching (2) or (3). Someone who works around (1) hits (2) only on their second invocation, which reads as a flake. (3) only manifests under the lock race that (2) creates — fixing (2) alone would have made (3) rare rather than permanent. The package's existing tests are pure unit tests over IDs and validation and never exercise the server lifecycle.

Summary by CodeRabbit

  • Bug Fixes

    • Improved server startup reliability by confirming readiness before recording server state.
    • Concurrent startups now safely detect and retain the first healthy server.
    • Stale, invalid, or non-serving process and port information is rejected.
    • Failed or duplicate startups are cleaned up automatically.
    • Database connections now consistently use the correct authentication account, avoiding failures when commit author details are configured separately.
  • Tests

    • Added coverage for process liveness, stale state, startup validation, concurrent startup handling, and cleanup.

Co-Authored-By: SageOx <ox@sageox.ai>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Cart server startup now validates process liveness and authenticated port readiness. Concurrent starters detect a winner, clean up losing or failed child processes, and publish state only after readiness. Tests cover these paths. Dolt connections use root for SQL authentication.

Changes

Server lifecycle validation

Layer / File(s) Summary
Process liveness and port reuse
internal/carts/proc_unix.go, internal/carts/proc_windows.go, internal/carts/server.go, internal/carts/server_test.go
runningServerPort checks PID liveness and authenticated port readiness before reuse. Tests cover stale, invalid, missing, and healthy state.
Concurrent readiness-gated startup
internal/carts/server.go, internal/carts/server_test.go
startServer waits for its process or a peer winner, publishes metadata after readiness, and kills and reaps failed or losing children. Tests cover winner detection, timeout, and state preservation.

Dolt SQL authentication

Layer / File(s) Summary
Root SQL authentication
internal/carts/store.go
New always uses root for MySQL/Dolt DSN authentication while keeping CommitterName for commit metadata.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant startServer
  participant ChildProcess
  participant ReadinessProbe
  participant StateFiles
  startServer->>ChildProcess: start server process
  startServer->>ReadinessProbe: wait for local or peer readiness
  alt local server is ready
    ReadinessProbe-->>startServer: local port
    startServer->>StateFiles: publish PID and port
  else peer server is ready
    ReadinessProbe-->>startServer: peer port
    startServer->>ChildProcess: kill and reap child
  else readiness fails
    ReadinessProbe-->>startServer: timeout or error
    startServer->>ChildProcess: kill and reap child
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes to Dolt server reuse, authentication, and startup race handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/carts-dolt-server-lifecycle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/carts/server_test.go (1)

62-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the live-PID, non-serving-port path.

The stale-PID case exits at Signal(0), so it never exercises the new readiness check. Add a table-driven case using the test process PID plus an intentionally closed local port, and assert runningServerPort rejects it.

As per coding guidelines, *_test.go tests should “use table-driven tests” and “test error paths.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/carts/server_test.go` around lines 62 - 84, The existing tests cover
stale and missing state, but not a live PID whose recorded port is not serving.
Extend the runningServerPort tests with a table-driven case using the current
test process PID and an intentionally closed local port, then assert that
runningServerPort returns an error for that case while preserving the existing
error-path coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/carts/server_test.go`:
- Around line 41-55: Update the “reaped process is detected as dead” and related
liveness subtests to spawn the current test binary via os.Args[0] with a no-op
test filter instead of relying on the external “true” executable. Replace the
conditional t.Skipf path with a test failure when cmd.Start cannot start,
ensuring the liveness assertions always execute.

In `@internal/carts/server.go`:
- Around line 114-126: Extend the cleanup lifecycle around the process started
by cmd.Start: arm a deferred reap-and-state-cleanup handler immediately after
startup, and disarm it only after both subsequent state-file writes complete
successfully. On either write failure, remove any partial state, terminate and
wait for the child, and return the write error wrapped with contextual
information.
- Around line 114-125: The startup path around waitForServer must handle
concurrent callers so a loser does not return an error when another process has
successfully started Dolt. Serialize startup with an inter-process lock, or
after killing and reaping the failed process, re-read runningServerPort and
return the existing winner’s port when available; otherwise preserve the current
startup error.

---

Nitpick comments:
In `@internal/carts/server_test.go`:
- Around line 62-84: The existing tests cover stale and missing state, but not a
live PID whose recorded port is not serving. Extend the runningServerPort tests
with a table-driven case using the current test process PID and an intentionally
closed local port, then assert that runningServerPort returns an error for that
case while preserving the existing error-path coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06fb8aa8-283d-451b-9735-39034d19b596

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3d486 and 2366956.

📒 Files selected for processing (3)
  • internal/carts/server.go
  • internal/carts/server_test.go
  • internal/carts/store.go

Comment thread internal/carts/server_test.go Outdated
Comment thread internal/carts/server.go
Comment thread internal/carts/server.go
Comment thread internal/carts/server.go Outdated
Comment thread internal/carts/server.go Outdated
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Greptile Summary

This change improves carts Dolt server startup and reuse by using the root SQL account, checking server availability before reuse, and allowing concurrent starters to join the winning server. A focused runtime check found that the shutdown path can still interrupt an unrelated local process when stale carts metadata names a recycled PID.

Confidence Score: 3/5

Not safe to merge until T-Rex findings are addressed.

The startup and reuse behavior is improved, but a reproduced cleanup failure can terminate an unrelated local process when stale metadata references a live recycled PID.

T-Rex reproduced 2 failing behaviors at runtime in internal/carts/server.go; the change needs fixes before it is safe to merge.

Files Needing Attention: internal/carts/server.go

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for a posted P1 finding and attached a Go repro and a log showing the StopServer interruption of the unrelated helper.
  • T-Rex produced a second proof for another posted P1 finding.
  • Contract validation was performed by running the repro; it confirmed StopServer interrupted the unrelated helper and removed stale dolt-server pid/port metadata after the interrupt, and the temporary test was removed with its source retained as an uploaded artifact.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (2)

  1. internal/carts/server.go, line 256-263 (link)

    P1 Stale server metadata interrupts an unrelated process

    StopServer trusts the numeric PID in dolt-server.pid and sends it os.Interrupt without verifying that it is the carts Dolt server or that it serves the recorded port. If a stale state file contains a recycled PID, this cleanup path terminates the unrelated live process and then removes the metadata. Verify server identity before signaling the PID; when the state is stale, avoid signaling the referenced process.

    Artifacts

    Temporary Go repro source for stale metadata naming an unrelated live helper

    • This Go-only test starts a controlled non-Dolt helper, records its PID and a port in carts metadata, invokes the real StopServer, and asserts signal delivery plus metadata removal; it demonstrates the exact failure setup.

    Passing command output confirming StopServer interrupted the unrelated helper

    • Captured output from the focused Go test run in `/home/user/repo` shows the helper was interrupted and stale PID and port files were deleted; the failure hypothesis is confirmed.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Claude Code

  2. General comment

    P1 StopServer can interrupt an unrelated process referenced by stale PID metadata

    • Bug
      • StopServer trusts the numeric contents of dolt-server.pid. If stale carts state contains a PID that has been recycled by the OS (or otherwise points at a live non-Dolt process), it sends that process os.Interrupt and then deletes both state files. The executed Go-only repro created exactly this state and observed the controlled unrelated helper receive SIGINT, exit, and have the metadata removed.
    • Cause
      • At internal/carts/server.go:247-264, StopServer parses the PID, obtains it with os.FindProcess, and calls proc.Signal(os.Interrupt) without validating that this process is the recorded Dolt SQL server or that it is serving the recorded port. os.FindProcess does not establish process ownership/identity.
    • Fix
      • Before signaling, validate the recorded server identity using the same PID-and-port liveness/ownership safeguards used by reuse, preferably with process identity data that survives PID reuse (such as a process start time or server-owned authenticated probe/token). If validation fails, do not signal the PID; remove only stale metadata when it is safe to do so.

    T-Rex Ran code and verified through T-Rex

Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(carts): never delete a peer's server..." | Re-trigger Greptile

…rite fail

Co-Authored-By: SageOx <ox@sageox.ai>
Comment thread internal/carts/server.go Outdated
…rt race

Co-Authored-By: SageOx <ox@sageox.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/carts/server.go (1)

162-184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reduce polling overhead in the peer-check path.

Each iteration of waitForOwnServerOrWinner calls runningServerPort to check for a peer. runningServerPort itself calls waitForServer with its own 2-second internal retry loop. If a peer's PID is alive but its port is not yet serving, a single outer iteration can block up to roughly 2.2 seconds instead of the intended 200ms cadence.

This does not break correctness (the 30-second outer deadline is still honored), but it slows winner detection in exactly the case this function is meant to speed up.

Use a single-shot peer probe, for example by adding a runningServerPortOnce variant (or a timeout parameter to runningServerPort) that skips the internal retry when called from this poll loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/carts/server.go` around lines 162 - 184, Reduce blocking in
waitForOwnServerOrWinner by using a single-shot peer lookup instead of
runningServerPort’s 2-second retry behavior. Add a runningServerPortOnce variant
or equivalent timeout-aware path that performs one immediate peer probe, and
call it from the polling loop while preserving the existing 200ms cadence and
timeout behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/carts/server.go`:
- Around line 217-232: Update pingServer’s MySQL DSN to include short
connection, read, and write timeouts (for example, 1s each) before calling
db.Ping. Keep the existing root authentication, host/port targeting, and cleanup
behavior unchanged so all probeServer callers fail promptly for unresponsive
peers.

---

Nitpick comments:
In `@internal/carts/server.go`:
- Around line 162-184: Reduce blocking in waitForOwnServerOrWinner by using a
single-shot peer lookup instead of runningServerPort’s 2-second retry behavior.
Add a runningServerPortOnce variant or equivalent timeout-aware path that
performs one immediate peer probe, and call it from the polling loop while
preserving the existing 200ms cadence and timeout behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9131cc93-d319-4059-ba36-58147c29c71c

📥 Commits

Reviewing files that changed from the base of the PR and between 2366956 and 84c9bc3.

📒 Files selected for processing (4)
  • internal/carts/proc_unix.go
  • internal/carts/proc_windows.go
  • internal/carts/server.go
  • internal/carts/server_test.go

Comment thread internal/carts/server.go
Comment on lines +217 to +232
// probeServer is the liveness probe used by the reuse and readiness paths.
// Indirected through a variable so tests can exercise the concurrency logic
// without standing up a real dolt sql-server to satisfy a MySQL handshake.
var probeServer = pingServer

// pingServer reports whether a dolt sql-server accepts an authenticated
// connection on host:port right now. Connects as root — dolt provisions no other
// account, and the git author name is not a SQL user.
func pingServer(host string, port int) error {
db, err := sql.Open("mysql", fmt.Sprintf("root@tcp(%s:%d)/", host, port))
if err != nil {
return err
}
defer db.Close()
return db.Ping()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)internal/carts/(server|store)\.go$|go\.mod$'

echo
echo "== server.go relevant outline/sections =="
if [ -f internal/carts/server.go ]; then
  wc -l internal/carts/server.go
  ast-grep outline internal/carts/server.go --match probeServer --view expanded || true
  sed -n '1,280p' internal/carts/server.go | cat -n
fi

echo
echo "== store.go relevant DSN usage =="
if [ -f internal/carts/store.go ]; then
  rg -n 'sql\.Open|dsn|timeout|readTimeout|writeTimeout|time\.Duration' internal/carts/store.go
  sed -n '1,220p' internal/carts/store.go | cat -n
fi

echo
echo "== go.mod pins =="
if [ -f go.mod ]; then
  rg -n 'github.com/go-sql-driver/mysql|github.com/jmoiron/sqlx|dolt|module ' go.mod
fi

echo
echo "== all mysql DSNs =="
rg -n 'sql\.Open\("mysql"|timeout=|readTimeout=|writeTimeout=' internal

Repository: sageox/ox

Length of output: 19546


🌐 Web query:

github.com/go-sql-driver/mysql DSN timeout readTimeout writeTimeout documentation

💡 Result:

The go-sql-driver/mysql library supports three distinct timeout parameters in its Data Source Name (DSN) string, all of which accept a string consisting of a decimal number followed by a unit suffix (e.g., "30s", "0.5m", "1m30s") [1][2]. 1. timeout: This is the driver-side connection (dial) timeout [1][3]. It specifies how long the driver will wait to establish the initial network connection to the MySQL server [1][4]. It does not govern the duration of the entire query execution or interaction after the connection is established [4][5]. 2. readTimeout: This parameter sets the I/O read timeout for the connection [6][2]. It controls how long the driver will wait to read data from the MySQL server after a command has been sent [2][7]. Its default value is 0 (no timeout) [2][7]. 3. writeTimeout: This parameter sets the I/O write timeout for the connection [6][2]. It controls how long the driver will wait to write data to the MySQL server [2][7]. Its default value is 0 (no timeout) [2][7]. These parameters are configured within the DSN string, often using the format user:password@tcp(address)/dbname?timeout=30s&readTimeout=10s&writeTimeout=10s [8]. Note that these are distinct from the connection pool settings managed by the standard library's database/sql package, such as SetConnMaxLifetime, which governs how long a connection may be reused [1][4].

Citations:


Add DSN timeouts to pingServer.

probeServer used by runningServerPort, waitForOwnServerOrWinner, and waitForServer opens MySQL without timeout, readTimeout, or writeTimeout. A slow/unresponsive peer can make db.Ping() block indefinitely, so add short probe timeouts such as timeout=1s&readTimeout=1s&writeTimeout=1s.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/carts/server.go` around lines 217 - 232, Update pingServer’s MySQL
DSN to include short connection, read, and write timeouts (for example, 1s each)
before calling db.Ping. Keep the existing root authentication, host/port
targeting, and cleanup behavior unchanged so all probeServer callers fail
promptly for unresponsive peers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant