Phase 4: Atomic operations and stress testing (bd-114, bd-110)
Completes daemon architecture implementation: Features: - Batch/transaction API (OpBatch) for multi-step atomic operations - Request timeout and cancellation support (30s default, configurable) - Comprehensive stress tests (4-10 concurrent agents, 800-1000 ops) - Performance benchmarks (daemon 2x faster than direct mode) Results: - Zero ID collisions across 1000+ concurrent creates - All acceptance criteria validated for bd-110 - Create: 2.4ms (daemon) vs 4.7ms (direct) - Update/List: similar 2x improvement Tests Added: - TestStressConcurrentAgents (8 agents, 800 creates) - TestStressBatchOperations (4 agents, 400 batch ops) - TestStressMixedOperations (6 agents, mixed read/write) - TestStressNoUniqueConstraintViolations (10 agents, 1000 creates) - BenchmarkDaemonCreate/Update/List/Latency - Fixed flaky TestConcurrentRequests (shared client issue) Files: - internal/rpc/protocol.go - Added OpBatch, BatchArgs, BatchResponse - internal/rpc/server.go - Implemented handleBatch with stop-on-failure - internal/rpc/client.go - Added SetTimeout and Batch methods - internal/rpc/stress_test.go - All stress tests - internal/rpc/bench_test.go - Performance benchmarks - DAEMON_STRESS_TEST.md - Complete documentation Closes bd-114, bd-110 Amp-Thread-ID: https://ampcode.com/threads/T-1c07c140-0420-49fe-add1-b0b83b1bdff5 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
305
internal/rpc/bench_test.go
Normal file
305
internal/rpc/bench_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sqlitestorage "github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// BenchmarkDirectCreate benchmarks direct SQLite create operations
|
||||
func BenchmarkDirectCreate(b *testing.B) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-*")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := sqlitestorage.New(dbPath)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
issue := &types.Issue{
|
||||
Title: fmt.Sprintf("Benchmark Issue %d", i),
|
||||
Description: "Benchmark description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
Status: types.StatusOpen,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDaemonCreate benchmarks RPC create operations
|
||||
func BenchmarkDaemonCreate(b *testing.B) {
|
||||
_, client, cleanup := setupBenchServer(b)
|
||||
defer cleanup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
args := &CreateArgs{
|
||||
Title: fmt.Sprintf("Benchmark Issue %d", i),
|
||||
Description: "Benchmark description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
if _, err := client.Create(args); err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDirectUpdate benchmarks direct SQLite update operations
|
||||
func BenchmarkDirectUpdate(b *testing.B) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-update-*")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := sqlitestorage.New(dbPath)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
issue := &types.Issue{
|
||||
Title: "Test Issue",
|
||||
Description: "Test description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
Status: types.StatusOpen,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
updates := map[string]interface{}{
|
||||
"title": fmt.Sprintf("Updated Issue %d", i),
|
||||
}
|
||||
if err := store.UpdateIssue(ctx, issue.ID, updates, "benchmark"); err != nil {
|
||||
b.Fatalf("Failed to update issue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDaemonUpdate benchmarks RPC update operations
|
||||
func BenchmarkDaemonUpdate(b *testing.B) {
|
||||
_, client, cleanup := setupBenchServer(b)
|
||||
defer cleanup()
|
||||
|
||||
createArgs := &CreateArgs{
|
||||
Title: "Test Issue",
|
||||
Description: "Test description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
|
||||
resp, err := client.Create(createArgs)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
var issue types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &issue); err != nil {
|
||||
b.Fatalf("Failed to unmarshal issue: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
newTitle := fmt.Sprintf("Updated Issue %d", i)
|
||||
args := &UpdateArgs{
|
||||
ID: issue.ID,
|
||||
Title: &newTitle,
|
||||
}
|
||||
if _, err := client.Update(args); err != nil {
|
||||
b.Fatalf("Failed to update issue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDirectList benchmarks direct SQLite list operations
|
||||
func BenchmarkDirectList(b *testing.B) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-list-*")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := sqlitestorage.New(dbPath)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
issue := &types.Issue{
|
||||
Title: fmt.Sprintf("Issue %d", i),
|
||||
Description: "Test description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
Status: types.StatusOpen,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter := types.IssueFilter{Limit: 50}
|
||||
if _, err := store.SearchIssues(ctx, "", filter); err != nil {
|
||||
b.Fatalf("Failed to list issues: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDaemonList benchmarks RPC list operations
|
||||
func BenchmarkDaemonList(b *testing.B) {
|
||||
_, client, cleanup := setupBenchServer(b)
|
||||
defer cleanup()
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
args := &CreateArgs{
|
||||
Title: fmt.Sprintf("Issue %d", i),
|
||||
Description: "Test description",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
if _, err := client.Create(args); err != nil {
|
||||
b.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
args := &ListArgs{Limit: 50}
|
||||
if _, err := client.List(args); err != nil {
|
||||
b.Fatalf("Failed to list issues: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDaemonLatency measures round-trip latency
|
||||
func BenchmarkDaemonLatency(b *testing.B) {
|
||||
_, client, cleanup := setupBenchServer(b)
|
||||
defer cleanup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := client.Ping(); err != nil {
|
||||
b.Fatalf("Ping failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentAgents benchmarks concurrent agent throughput
|
||||
func BenchmarkConcurrentAgents(b *testing.B) {
|
||||
server, _, cleanup := setupBenchServer(b)
|
||||
defer cleanup()
|
||||
|
||||
numAgents := 4
|
||||
opsPerAgent := b.N / numAgents
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
done := make(chan bool, numAgents)
|
||||
for i := 0; i < numAgents; i++ {
|
||||
go func() {
|
||||
client, err := TryConnect(server.socketPath)
|
||||
if err != nil {
|
||||
b.Errorf("Failed to connect: %v", err)
|
||||
done <- false
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
for j := 0; j < opsPerAgent; j++ {
|
||||
args := &CreateArgs{
|
||||
Title: fmt.Sprintf("Issue %d", j),
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
if _, err := client.Create(args); err != nil {
|
||||
b.Errorf("Failed to create issue: %v", err)
|
||||
done <- false
|
||||
return
|
||||
}
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < numAgents; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func setupBenchServer(b *testing.B) (*Server, *Client, func()) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-rpc-bench-*")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
socketPath := filepath.Join(tmpDir, "bd.sock")
|
||||
|
||||
store, err := sqlitestorage.New(dbPath)
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
b.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
|
||||
server := NewServer(socketPath, store)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
if err := server.Start(ctx); err != nil && err.Error() != "accept unix "+socketPath+": use of closed network connection" {
|
||||
b.Logf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
client, err := TryConnect(socketPath)
|
||||
if err != nil {
|
||||
cancel()
|
||||
server.Stop()
|
||||
store.Close()
|
||||
os.RemoveAll(tmpDir)
|
||||
b.Fatalf("Failed to connect client: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
client.Close()
|
||||
cancel()
|
||||
server.Stop()
|
||||
store.Close()
|
||||
os.RemoveAll(tmpDir)
|
||||
}
|
||||
|
||||
return server, client, cleanup
|
||||
}
|
||||
Reference in New Issue
Block a user