From 98c4cede7ded9c47b78a28d919e699bbcece6668 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 6 May 2026 13:00:06 +0530 Subject: [PATCH 1/7] add files to debug --- .../spanner/issue/benchmark-insert-repl.ts | 326 +++++++++++++ handwritten/spanner/issue/create.sql | 20 + handwritten/spanner/issue/main.go | 433 ++++++++++++++++++ 3 files changed, 779 insertions(+) create mode 100644 handwritten/spanner/issue/benchmark-insert-repl.ts create mode 100644 handwritten/spanner/issue/create.sql create mode 100644 handwritten/spanner/issue/main.go diff --git a/handwritten/spanner/issue/benchmark-insert-repl.ts b/handwritten/spanner/issue/benchmark-insert-repl.ts new file mode 100644 index 00000000000..8f0869cbe20 --- /dev/null +++ b/handwritten/spanner/issue/benchmark-insert-repl.ts @@ -0,0 +1,326 @@ +/** + * REPL-friendly benchmark script for DeviceRecentActivityLog table.insert() + * + * Dependencies (install with npm): + * - @google-cloud/spanner + * - @grpc/grpc-js + * + * Usage in k8s pod: + * 1. kubectl exec -it -- node + * 2. Paste this entire file + * 3. await runBenchmark() + */ + +/* eslint-disable no-console */ +/* eslint-disable eslint-plugin-node/no-process-env */ +/* eslint-disable no-await-in-loop */ + +// Disable multiplexed sessions to use traditional session pool +// This avoids "ReleaseError: Unable to release unknown resource" with SDK v8.6.0 +// process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false' + +// Benchmark configuration - can be overridden via environment variables +const SAMPLE_SIZE = parseInt(process.env.SAMPLE_SIZE || '10000', 10) +const INSERT_COUNT = parseInt(process.env.INSERT_COUNT || '1000', 10) +const INSERT_CONCURRENCY = parseInt(process.env.INSERT_CONCURRENCY || '110', 10) +const BATCH_COUNT = parseInt(process.env.BATCH_COUNT || '1', 10) +const DUPLICATE_INSERT = process.env.DUPLICATE_INSERT === 'true' + +// Database configuration +const DB_PROJECT_ID = process.env.DB_PROJECT_ID || 'emulator' +const DB_INSTANCE = process.env.DB_INSTANCE || 'device-tracking' +const DB_DATABASE = process.env.DB_DATABASE || 'device-tracking' +const DB_SCHEMA = process.env.DB_SCHEMA || 'tracking' +const POOL_OPTIONS = process.env.POOL_OPTIONS + ? JSON.parse(process.env.POOL_OPTIONS) + : { + max: 20, + min: 1, + incStep: 5, + maxIdle: 1, + idlesAfter: 1, + keepAlive: 10, + acquireTimeout: 10_000, + fail: false, + } + +async function runBenchmark() { + // Dynamic imports for REPL compatibility + const { randomUUID, subtle } = await import('node:crypto') + const { Spanner } = await import('@google-cloud/spanner') + const { status: Status } = await import('@grpc/grpc-js') + + // ====== INLINE UTILITIES ====== + + async function sha256Hash(str: string | undefined): Promise { + const encoder = new TextEncoder() + return Array.from( + new Uint8Array(await subtle.digest('SHA-256', encoder.encode(str))), + ) + .map((b) => `00${b.toString(16)}`.slice(-2)) + .join('') + } + + async function createDeviceRecentActivityLogId(opts: { + deviceId: string + deviceDetailsId: string + httpRequestDetailsId: string + createdAt: Date + sessionId?: string | null | undefined + }): Promise { + return sha256Hash( + `${opts.deviceId}${opts.deviceDetailsId}${opts.httpRequestDetailsId}${opts.createdAt.getTime()}${opts.sessionId ?? ''}`, + ) + } + + function ipToBytes(ipAddress: string): Buffer { + const octets = ipAddress.split('.') + if (octets.length === 4) { + const bytes = Buffer.alloc(4) + for (let i = 0; i < 4; i++) { + bytes[i] = parseInt(octets[i], 10) + } + return bytes + } + throw new Error('Invalid IP address format') + } + + async function asyncMap( + items: T[], + mapperFn: (item: T, index: number) => Promise, + opts?: { concurrency: number }, + ): Promise { + const concurrency = opts?.concurrency ?? Infinity + const results: R[] = new Array(items.length) + let currentIndex = 0 + + async function processNext(): Promise { + while (currentIndex < items.length) { + const index = currentIndex++ + results[index] = await mapperFn(items[index], index) + } + } + + const workers = Array.from( + { length: Math.min(concurrency, items.length) }, + () => processNext(), + ) + await Promise.all(workers) + return results + } + + async function createBenchmarkRecord(opts: { + index: number + numDevices: number + numDetails: number + numRequests: number + numLocations: number + }) { + // Generate IDs with controlled cardinality using modulo and SHA256 + const deviceRecordId = await sha256Hash( + `device-${opts.index % opts.numDevices}`, + ) + const deviceDetailsId = await sha256Hash( + `detail-${opts.index % opts.numDetails}`, + ) + const httpRequestDetailsId = await sha256Hash( + `request-${opts.index % opts.numRequests}`, + ) + const httpRequestLocationId = + opts.numLocations > 0 + ? await sha256Hash(`location-${opts.index % opts.numLocations}`) + : null + + const xRequestId = randomUUID() + const createdAt = new Date(Date.now() + opts.index * 1000) + const sessionId = randomUUID() + + const deviceRecentActivityLogId = await createDeviceRecentActivityLogId({ + deviceId: `benchmark-${opts.index}`, + deviceDetailsId, + httpRequestDetailsId, + createdAt, + sessionId, + }) + + return { + deviceRecentActivityLogId, + deviceRecordId, + deviceDetailsId, + httpRequestDetailsId, + ipAddress: ipToBytes('192.168.1.100'), + institutionId: 'benchmark-institution', + userId: Math.random() > 0.5 ? 'user-123' : null, + username: null, + xRequestId, + httpRequestLocationId, + latency: Math.random() > 0.5 ? Math.floor(Math.random() * 100) : null, + sessionId, + createdAt, + } + } + + // ====== DATABASE CONNECTION ====== + + console.log('Initializing database connection...') + const spanner = new Spanner({ + projectId: DB_PROJECT_ID, + }) + + const instance = spanner.instance(DB_INSTANCE) + const poolOpts = + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS === 'false' + ? POOL_OPTIONS + : {} + const database = instance.database(DB_DATABASE, poolOpts) + + try { + console.log('Generating benchmark data...') + console.log( + `Configuration: SAMPLE_SIZE=${SAMPLE_SIZE}, INSERT_COUNT=${INSERT_COUNT}, INSERT_CONCURRENCY=${INSERT_CONCURRENCY}, BATCH_COUNT=${BATCH_COUNT}, DUPLICATE_INSERT=${DUPLICATE_INSERT}`, + ) + + // Generate IDs instead of querying - use sampleSize to control cardinality + const numDevices = SAMPLE_SIZE + const numDetails = SAMPLE_SIZE + const numRequests = SAMPLE_SIZE + const numLocations = SAMPLE_SIZE + + console.log(` Will generate IDs with cardinality: ${SAMPLE_SIZE}`) + console.log(` Devices: ${numDevices} unique`) + console.log(` Details: ${numDetails} unique`) + console.log(` Requests: ${numRequests} unique`) + console.log(` Locations: ${numLocations} unique`) + + const totalRecords = INSERT_COUNT * BATCH_COUNT + const totalInserts = DUPLICATE_INSERT ? totalRecords * 2 : totalRecords + console.log( + `\nCreating ${totalRecords} records in ${INSERT_COUNT} batches of ${BATCH_COUNT}...\n`, + ) + if (DUPLICATE_INSERT) { + console.log( + `Total insert operations: ${totalInserts} (${totalRecords} + ${totalRecords} duplicates)\n`, + ) + } else { + console.log(`Total insert operations: ${totalInserts} (no duplicates)\n`) + } + + const batches = [] + for (let i = 0; i < INSERT_COUNT; i++) { + const batch = [] + for (let j = 0; j < BATCH_COUNT; j++) { + const record = await createBenchmarkRecord({ + index: i * BATCH_COUNT + j, + numDevices, + numDetails, + numRequests, + numLocations, + }) + batch.push(record) + } + batches.push(batch) + } + + console.log( + `\nStarting ${INSERT_COUNT} batch inserts with concurrency: ${INSERT_CONCURRENCY}...\n`, + ) + + const durations: number[] = [] + let alreadyExistsCount = 0 + const overallStartTime = performance.now() + + await asyncMap( + batches, + async (batch, i) => { + const table = database.table(`${DB_SCHEMA}.DeviceRecentActivityLog`) + + const label = `batch-${i} (${BATCH_COUNT} records, concurrency: ${INSERT_CONCURRENCY})` + console.time(label) + const startTime = performance.now() + + console.log(label) + + // First insert - should succeed + try { + await table.insert(batch) + } catch (e: any) { + if (e.code === Status.ALREADY_EXISTS) { + alreadyExistsCount++ + console.log(` ${label} - ALREADY_EXISTS (expected on retry)`) + } else { + throw e + } + } + + if (DUPLICATE_INSERT) { + // Duplicate insert - should trigger ALREADY_EXISTS + try { + await table.insert(batch) + } catch (e: any) { + if (e.code === Status.ALREADY_EXISTS) { + alreadyExistsCount++ + console.log(` ${label} - ALREADY_EXISTS (expected on duplicate)`) + } else { + throw e + } + } + } + + const duration = performance.now() - startTime + console.timeEnd(label) + durations.push(duration) + + if ((i + 1) % 10 === 0) { + const avg = durations.slice(-10).reduce((a, b) => a + b, 0) / 10 + console.log( + ` [${i + 1}/${INSERT_COUNT}] Last 10 batches avg: ${avg.toFixed(2)}ms (${BATCH_COUNT} records/batch, ${INSERT_CONCURRENCY} concurrent)\n`, + ) + } + }, + { concurrency: INSERT_CONCURRENCY }, + ) + + const overallDuration = performance.now() - overallStartTime + const insertsPerSecond = (totalInserts / (overallDuration / 1000)).toFixed( + 2, + ) + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log(' Benchmark Summary') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log(` Total Batches: ${INSERT_COUNT}`) + console.log(` Batch Size: ${BATCH_COUNT}`) + console.log(` Total Records: ${totalRecords}`) + console.log( + ` Total Inserts: ${totalInserts} (includes ${alreadyExistsCount} duplicates)`, + ) + console.log(` Concurrency: ${INSERT_CONCURRENCY}`) + console.log(` Total Time: ${overallDuration.toFixed(2)}ms`) + console.log(` Inserts/Second: ${insertsPerSecond}`) + console.log( + ` Avg Batch Duration: ${(durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2)}ms`, + ) + console.log(` Min Batch Duration: ${Math.min(...durations).toFixed(2)}ms`) + console.log(` Max Batch Duration: ${Math.max(...durations).toFixed(2)}ms`) + + durations.sort((a, b) => a - b) + const p50 = durations[Math.floor(durations.length * 0.5)] + const p90 = durations[Math.floor(durations.length * 0.9)] + const p95 = durations[Math.floor(durations.length * 0.95)] + const p99 = durations[Math.floor(durations.length * 0.99)] + + console.log(` P50: ${p50.toFixed(2)}ms`) + console.log(` P90: ${p90.toFixed(2)}ms`) + console.log(` P95: ${p95.toFixed(2)}ms`) + console.log(` P99: ${p99.toFixed(2)}ms`) + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n') + } finally { + await database.close() + } +} + +// Export for REPL usage +;(globalThis as any).runBenchmark = runBenchmark +console.log('✓ Benchmark loaded. Run with: await runBenchmark()') + +await runBenchmark() diff --git a/handwritten/spanner/issue/create.sql b/handwritten/spanner/issue/create.sql new file mode 100644 index 00000000000..4eee905c180 --- /dev/null +++ b/handwritten/spanner/issue/create.sql @@ -0,0 +1,20 @@ +CREATE TABLE + tracking.DeviceRecentActivityLog ( deviceRecentActivityLogId STRING(64) NOT NULL, + deviceRecordId STRING(64) NOT NULL, + httpRequestDetailsId STRING(64) NOT NULL, + deviceDetailsId STRING(64) NOT NULL, + ipAddress BYTES(24) NOT NULL, + ipAddressText STRING(MAX) AS (NET.IP_TO_STRING(ipAddress)), + xRequestId STRING(64) NOT NULL, + institutionId STRING(64), + userId STRING(64), + username STRING(64), + httpRequestLocationId STRING(64), + latency INT64, + createdAt TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()), + sessionId STRING(256), + ) +PRIMARY KEY + (deviceRecentActivityLogId), + ROW DELETION POLICY (OLDER_THAN(createdAt, + INTERVAL 20 DAY)); diff --git a/handwritten/spanner/issue/main.go b/handwritten/spanner/issue/main.go new file mode 100644 index 00000000000..fbbaa25dd4c --- /dev/null +++ b/handwritten/spanner/issue/main.go @@ -0,0 +1,433 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "math" + mathrand "math/rand" + "net" + "os" + "sort" + "strconv" + "sync" + "time" + + "cloud.google.com/go/spanner" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" +) + +type BenchmarkRecord struct { + DeviceRecentActivityLogID string + DeviceRecordID string + DeviceDetailsID string + HTTPRequestDetailsID string + IPAddress []byte + InstitutionID string + UserID *string + Username *string + XRequestID string + HTTPRequestLocationID *string + Latency *int64 + SessionID string + CreatedAt time.Time +} + +func main() { + ctx := context.Background() + + // Benchmark configuration from environment variables + sampleSize := getEnvInt("SAMPLE_SIZE", 10_000) + insertCount := getEnvInt("INSERT_COUNT", 1_000) + insertConcurrency := getEnvInt("INSERT_CONCURRENCY", 110) + batchCount := getEnvInt("BATCH_COUNT", 1) + + // Database configuration from environment variables + dbProjectID := getEnv("DB_PROJECT_ID", "emulator") + dbInstance := getEnv("DB_INSTANCE", "device-tracking") + dbDatabase := getEnv("DB_DATABASE", "device-tracking") + dbSchema := getEnv("DB_SCHEMA", "tracking") + duplicateInsert := getEnv("DUPLICATE_INSERT", "false") == "true" + + if err := runBenchmark(ctx, dbProjectID, dbInstance, dbDatabase, dbSchema, sampleSize, insertCount, insertConcurrency, batchCount, duplicateInsert); err != nil { + fmt.Fprintf(os.Stderr, "Benchmark failed: %v\n", err) + os.Exit(1) + } +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} + +func getEnvInt(key string, fallback int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return fallback +} + +func runBenchmark(ctx context.Context, projectID, instance, database, schema string, sampleSize, insertCount, insertConcurrency, batchCount int, duplicateInsert bool) error { + fmt.Println("Initializing database connection...") + fmt.Printf("Configuration: SAMPLE_SIZE=%d, INSERT_COUNT=%d, INSERT_CONCURRENCY=%d, BATCH_COUNT=%d, DUPLICATE_INSERT=%t\n", sampleSize, insertCount, insertConcurrency, batchCount, duplicateInsert) + + dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instance, database) + client, err := spanner.NewClient(ctx, dbPath) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + defer client.Close() + + // Fetch reference data samples + fmt.Println("Fetching reference data samples...") + deviceRecordIDs, err := fetchStringColumn(ctx, client, fmt.Sprintf("SELECT deviceRecordId FROM %s.Devices LIMIT %d", schema, sampleSize)) + if err != nil { + return fmt.Errorf("failed to fetch deviceRecordIds: %w", err) + } + + deviceDetailsIDs, err := fetchStringColumn(ctx, client, fmt.Sprintf("SELECT deviceDetailsId FROM %s.DeviceDetails LIMIT %d", schema, sampleSize)) + if err != nil { + return fmt.Errorf("failed to fetch deviceDetailsIds: %w", err) + } + + httpRequestDetailsIDs, err := fetchStringColumn(ctx, client, fmt.Sprintf("SELECT httpRequestDetailsId FROM %s.HttpRequestDetails LIMIT %d", schema, sampleSize)) + if err != nil { + return fmt.Errorf("failed to fetch httpRequestDetailsIds: %w", err) + } + + httpRequestLocationIDs, err := fetchStringColumn(ctx, client, fmt.Sprintf("SELECT httpRequestLocationId FROM %s.HttpRequestLocations LIMIT %d", schema, sampleSize)) + if err != nil { + return fmt.Errorf("failed to fetch httpRequestLocationIds: %w", err) + } + + fmt.Printf(" Sampled %d httpRequestLocationIds\n", len(httpRequestLocationIDs)) + + if len(deviceRecordIDs) == 0 || len(deviceDetailsIDs) == 0 || len(httpRequestDetailsIDs) == 0 { + return fmt.Errorf("error: No reference data found. Cannot proceed with benchmark") + } + + totalRecords := insertCount * batchCount + totalInserts := totalRecords + if duplicateInsert { + totalInserts = totalRecords * 2 // Each batch: 1 original + 1 duplicate + } + fmt.Printf("\nCreating %d records in %d batches of %d...\n", totalRecords, insertCount, batchCount) + if duplicateInsert { + fmt.Printf("Total insert operations: %d (%d + %d duplicates)\n\n", totalInserts, totalRecords, totalRecords) + } else { + fmt.Printf("Total insert operations: %d (no duplicates)\n\n", totalInserts) + } + + // Pre-generate all batches + batches := make([][]*BenchmarkRecord, insertCount) + for i := 0; i < insertCount; i++ { + batch := make([]*BenchmarkRecord, batchCount) + for j := 0; j < batchCount; j++ { + record, err := createBenchmarkRecord( + i*batchCount+j, + deviceRecordIDs, + deviceDetailsIDs, + httpRequestDetailsIDs, + httpRequestLocationIDs, + ) + if err != nil { + return fmt.Errorf("failed to create record: %w", err) + } + batch[j] = record + } + batches[i] = batch + } + + fmt.Printf("\nStarting %d batch inserts with concurrency: %d...\n\n", insertCount, insertConcurrency) + + durations := make([]float64, insertCount) + var alreadyExistsCount int64 + var mu sync.Mutex + + overallStart := time.Now() + + // Process batches with concurrency control + sem := make(chan struct{}, insertConcurrency) + var wg sync.WaitGroup + + for i, batch := range batches { + wg.Add(1) + sem <- struct{}{} // Acquire semaphore + + go func(index int, records []*BenchmarkRecord) { + defer wg.Done() + defer func() { <-sem }() // Release semaphore + + label := fmt.Sprintf("batch-%d (%d records, concurrency: %d)", index, batchCount, insertConcurrency) + fmt.Println(label) + + start := time.Now() + + // Build mutations for this batch + mutations := make([]*spanner.Mutation, len(records)) + for j, rec := range records { + mutations[j] = spanner.Insert( + schema+".DeviceRecentActivityLog", + []string{ + "deviceRecentActivityLogId", + "deviceRecordId", + "deviceDetailsId", + "httpRequestDetailsId", + "ipAddress", + "xRequestId", + "institutionId", + "userId", + "username", + "httpRequestLocationId", + "latency", + "sessionId", + "createdAt", + }, + []interface{}{ + rec.DeviceRecentActivityLogID, + rec.DeviceRecordID, + rec.DeviceDetailsID, + rec.HTTPRequestDetailsID, + rec.IPAddress, + rec.XRequestID, + rec.InstitutionID, + rec.UserID, + rec.Username, + rec.HTTPRequestLocationID, + rec.Latency, + rec.SessionID, + rec.CreatedAt, + }, + ) + } + + // First insert - should succeed + _, err := client.Apply(ctx, mutations) + if err != nil { + if spanner.ErrCode(err) == codes.AlreadyExists { + mu.Lock() + alreadyExistsCount++ + mu.Unlock() + fmt.Printf(" %s - ALREADY_EXISTS (expected on retry)\n", label) + } else { + fmt.Fprintf(os.Stderr, "Error on first insert: %v\n", err) + } + } + + if (duplicateInsert) { + // Duplicate insert - should trigger ALREADY_EXISTS + _, err = client.Apply(ctx, mutations) + if err != nil { + if spanner.ErrCode(err) == codes.AlreadyExists { + mu.Lock() + alreadyExistsCount++ + mu.Unlock() + fmt.Printf(" %s - ALREADY_EXISTS (expected on duplicate)\n", label) + } else { + fmt.Fprintf(os.Stderr, "Error on duplicate insert: %v\n", err) + } + } + } + + duration := time.Since(start).Milliseconds() + mu.Lock() + durations[index] = float64(duration) + mu.Unlock() + + if (index+1)%10 == 0 { + mu.Lock() + // Calculate average of last 10 batches + windowStart := index - 9 + if windowStart < 0 { + windowStart = 0 + } + var sum float64 + count := 0 + for k := windowStart; k <= index; k++ { + sum += durations[k] + count++ + } + avg := sum / float64(count) + mu.Unlock() + fmt.Printf(" [%d/%d] Last %d batches avg: %.2fms (%d records/batch, %d concurrent)\n\n", + index+1, insertCount, count, avg, batchCount, insertConcurrency) + } + }(i, batch) + } + + wg.Wait() + + overallDuration := time.Since(overallStart).Milliseconds() + insertsPerSecond := float64(totalInserts) / (float64(overallDuration) / 1000.0) + + // Calculate statistics + var sum float64 + min := durations[0] + max := durations[0] + for _, d := range durations { + sum += d + if d < min { + min = d + } + if d > max { + max = d + } + } + avg := sum / float64(len(durations)) + + // Calculate percentiles + sorted := make([]float64, len(durations)) + copy(sorted, durations) + sort.Float64s(sorted) + + p50 := sorted[int(float64(len(sorted))*0.5)] + p90 := sorted[int(float64(len(sorted))*0.9)] + p95 := sorted[int(float64(len(sorted))*0.95)] + p99 := sorted[int(math.Min(float64(len(sorted))*0.99, float64(len(sorted)-1)))] + + // Print summary + fmt.Println("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Println(" Benchmark Summary") + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Printf(" Total Batches: %d\n", insertCount) + fmt.Printf(" Batch Size: %d\n", batchCount) + fmt.Printf(" Total Records: %d\n", totalRecords) + fmt.Printf(" Total Inserts: %d (includes %d duplicates)\n", totalInserts, alreadyExistsCount) + fmt.Printf(" Concurrency: %d\n", insertConcurrency) + fmt.Printf(" Total Time: %.2fms\n", float64(overallDuration)) + fmt.Printf(" Inserts/Second: %.2f\n", insertsPerSecond) + fmt.Printf(" Avg Batch Duration: %.2fms\n", avg) + fmt.Printf(" Min Batch Duration: %.2fms\n", min) + fmt.Printf(" Max Batch Duration: %.2fms\n", max) + fmt.Printf(" P50: %.2fms\n", p50) + fmt.Printf(" P90: %.2fms\n", p90) + fmt.Printf(" P95: %.2fms\n", p95) + fmt.Printf(" P99: %.2fms\n", p99) + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + + return nil +} + +func fetchStringColumn(ctx context.Context, client *spanner.Client, query string) ([]string, error) { + var results []string + stmt := spanner.Statement{SQL: query} + iter := client.Single().Query(ctx, stmt) + defer iter.Stop() + + for { + row, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + + var value string + if err := row.Columns(&value); err != nil { + return nil, err + } + results = append(results, value) + } + + return results, nil +} + +func createBenchmarkRecord( + index int, + deviceRecordIDs, deviceDetailsIDs, httpRequestDetailsIDs, httpRequestLocationIDs []string, +) (*BenchmarkRecord, error) { + deviceRecordID := selectRandom(deviceRecordIDs) + deviceDetailsID := selectRandom(deviceDetailsIDs) + httpRequestDetailsID := selectRandom(httpRequestDetailsIDs) + + var httpRequestLocationID *string + if len(httpRequestLocationIDs) > 0 { + id := selectRandom(httpRequestLocationIDs) + httpRequestLocationID = &id + } + + xRequestID := generateUUID() + sessionID := generateUUID() + createdAt := time.Now().Add(time.Duration(index) * time.Second) + + deviceID := fmt.Sprintf("benchmark-%d", index) + deviceRecentActivityLogID, err := createDeviceRecentActivityLogID( + deviceID, + deviceDetailsID, + httpRequestDetailsID, + createdAt, + sessionID, + ) + if err != nil { + return nil, err + } + + var userID *string + if mathrand.Float64() > 0.5 { + uid := "user-123" + userID = &uid + } + + var latency *int64 + if mathrand.Float64() > 0.5 { + lat := int64(mathrand.Intn(100)) + latency = &lat + } + + return &BenchmarkRecord{ + DeviceRecentActivityLogID: deviceRecentActivityLogID, + DeviceRecordID: deviceRecordID, + DeviceDetailsID: deviceDetailsID, + HTTPRequestDetailsID: httpRequestDetailsID, + IPAddress: ipToBytes("192.168.1.100"), + InstitutionID: "benchmark-institution", + UserID: userID, + Username: nil, + XRequestID: xRequestID, + HTTPRequestLocationID: httpRequestLocationID, + Latency: latency, + SessionID: sessionID, + CreatedAt: createdAt, + }, nil +} + +func createDeviceRecentActivityLogID(deviceID, deviceDetailsID, httpRequestDetailsID string, createdAt time.Time, sessionID string) (string, error) { + input := fmt.Sprintf("%s%s%s%d%s", deviceID, deviceDetailsID, httpRequestDetailsID, createdAt.UnixMilli(), sessionID) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:]), nil +} + +func ipToBytes(ipAddress string) []byte { + ip := net.ParseIP(ipAddress) + if ip == nil { + return nil + } + // Return IPv4 as 4 bytes + if ip4 := ip.To4(); ip4 != nil { + return ip4 + } + // Return IPv6 as 16 bytes + return ip.To16() +} + +func selectRandom(items []string) string { + return items[mathrand.Intn(len(items))] +} + +func generateUUID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(fmt.Sprintf("failed to generate UUID: %v", err)) + } + b[6] = (b[6] & 0x0f) | 0x40 // Version 4 + b[8] = (b[8] & 0x3f) | 0x80 // Variant is 10 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} From 782ee17ab120707ea74477778447d1ce68af303d Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 6 May 2026 13:53:47 +0530 Subject: [PATCH 2/7] add docker files --- handwritten/spanner/issue/Dockerfile.go | 11 + handwritten/spanner/issue/Dockerfile.node | 10 + .../spanner/issue/Dockerfile.node-cluster | 11 + .../issue/Dockerfile.node-cluster-current | 28 ++ .../spanner/issue/Dockerfile.node-current | 27 ++ handwritten/spanner/issue/README.md | 47 +++ handwritten/spanner/issue/build-images.sh | 47 +++ handwritten/spanner/issue/go/go.mod | 55 +++ handwritten/spanner/issue/go/go.sum | 208 +++++++++++ handwritten/spanner/issue/{ => go}/main.go | 0 .../issue/k8s/go-insert-benchmark.yaml | 43 +++ .../k8s/node-cluster-insert-benchmark.yaml | 47 +++ ...node-current-cluster-insert-benchmark.yaml | 47 +++ .../k8s/node-current-insert-benchmark.yaml | 49 +++ .../issue/k8s/node-insert-benchmark.yaml | 45 +++ handwritten/spanner/issue/node/benchmark.js | 348 ++++++++++++++++++ .../spanner/issue/node/cluster-runner.js | 59 +++ 17 files changed, 1082 insertions(+) create mode 100644 handwritten/spanner/issue/Dockerfile.go create mode 100644 handwritten/spanner/issue/Dockerfile.node create mode 100644 handwritten/spanner/issue/Dockerfile.node-cluster create mode 100644 handwritten/spanner/issue/Dockerfile.node-cluster-current create mode 100644 handwritten/spanner/issue/Dockerfile.node-current create mode 100644 handwritten/spanner/issue/README.md create mode 100755 handwritten/spanner/issue/build-images.sh create mode 100644 handwritten/spanner/issue/go/go.mod create mode 100644 handwritten/spanner/issue/go/go.sum rename handwritten/spanner/issue/{ => go}/main.go (100%) create mode 100644 handwritten/spanner/issue/k8s/go-insert-benchmark.yaml create mode 100644 handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml create mode 100644 handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml create mode 100644 handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml create mode 100644 handwritten/spanner/issue/k8s/node-insert-benchmark.yaml create mode 100644 handwritten/spanner/issue/node/benchmark.js create mode 100644 handwritten/spanner/issue/node/cluster-runner.js diff --git a/handwritten/spanner/issue/Dockerfile.go b/handwritten/spanner/issue/Dockerfile.go new file mode 100644 index 00000000000..ea0a28136f7 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.go @@ -0,0 +1,11 @@ +FROM golang:1.25-bookworm AS build +WORKDIR /src +COPY issue/go/go.mod ./go.mod +COPY issue/go/go.sum ./go.sum +RUN go mod download +COPY issue/go/main.go ./main.go +RUN CGO_ENABLED=0 GOOS=linux go build -o /out/insert-benchmark ./main.go + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /out/insert-benchmark /insert-benchmark +ENTRYPOINT ["/insert-benchmark"] diff --git a/handwritten/spanner/issue/Dockerfile.node b/handwritten/spanner/issue/Dockerfile.node new file mode 100644 index 00000000000..3d3bced122e --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.node @@ -0,0 +1,10 @@ +FROM node:22-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +ARG SPANNER_VERSION=8.6.0 +RUN npm init -y \ + && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js tsx \ + && npm cache clean --force +COPY issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +USER node +ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster b/handwritten/spanner/issue/Dockerfile.node-cluster new file mode 100644 index 00000000000..6a26d832692 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.node-cluster @@ -0,0 +1,11 @@ +FROM node:22-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +ARG SPANNER_VERSION=8.6.0 +RUN npm init -y \ + && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js \ + && npm cache clean --force +COPY issue/node/benchmark.js ./benchmark.js +COPY issue/node/cluster-runner.js ./cluster-runner.js +USER node +ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster-current b/handwritten/spanner/issue/Dockerfile.node-cluster-current new file mode 100644 index 00000000000..9111021cb65 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.node-cluster-current @@ -0,0 +1,28 @@ +FROM node:22-bookworm-slim AS local-lib +WORKDIR /workspace/spanner +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then \ + npm ci --ignore-scripts; \ + else \ + npm install --ignore-scripts; \ + fi +COPY . . +RUN npm run compile \ + && mkdir -p /tmp/spanner-pack \ + && npm pack --pack-destination /tmp/spanner-pack \ + && cp /tmp/spanner-pack/google-cloud-spanner-*.tgz /tmp/google-cloud-spanner.tgz + +FROM node:22-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +RUN npm init -y \ + && npm install --omit=dev @grpc/grpc-js \ + && npm cache clean --force +COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tgz +RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ + && npm cache clean --force \ + && node -e "console.log(require('@google-cloud/spanner/package.json').version)" +COPY issue/node/benchmark.js ./benchmark.js +COPY issue/node/cluster-runner.js ./cluster-runner.js +USER node +ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-current b/handwritten/spanner/issue/Dockerfile.node-current new file mode 100644 index 00000000000..d4671ea2b08 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.node-current @@ -0,0 +1,27 @@ +FROM node:22-bookworm-slim AS local-lib +WORKDIR /workspace/spanner +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then \ + npm ci --ignore-scripts; \ + else \ + npm install --ignore-scripts; \ + fi +COPY . . +RUN npm run compile \ + && mkdir -p /tmp/spanner-pack \ + && npm pack --pack-destination /tmp/spanner-pack \ + && cp /tmp/spanner-pack/google-cloud-spanner-*.tgz /tmp/google-cloud-spanner.tgz + +FROM node:22-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +RUN npm init -y >/dev/null 2>&1 || true \ + && npm install --omit=dev @grpc/grpc-js tsx \ + && npm cache clean --force +COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tgz +RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ + && npm cache clean --force \ + && node -e "console.log(require('@google-cloud/spanner/package.json').version)" +COPY issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +USER node +ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] diff --git a/handwritten/spanner/issue/README.md b/handwritten/spanner/issue/README.md new file mode 100644 index 00000000000..f11e1ea8fe7 --- /dev/null +++ b/handwritten/spanner/issue/README.md @@ -0,0 +1,47 @@ +# Insert benchmark issue repro + +Build and push all three images: + +```sh +IMAGE_REPO=us-central1-docker.pkg.dev/span-cloud-testing/irahul-images \ +SPANNER_VERSION=8.6.0 \ +BUILD_CURRENT=true \ +./issue/build-images.sh +``` + +Run jobs: + +Release baseline uses `issue-insert-node:latest` from npm `SPANNER_VERSION`. Current branch uses `issue-insert-node:current`. Release cluster uses `issue-insert-node-cluster:release-8.6.0`; current cluster uses `issue-insert-node-cluster:current`. + +```sh +kubectl apply -f issue/k8s/go-insert-benchmark.yaml +kubectl apply -f issue/k8s/node-insert-benchmark.yaml +kubectl apply -f issue/k8s/node-current-insert-benchmark.yaml +kubectl apply -f issue/k8s/node-cluster-insert-benchmark.yaml +kubectl apply -f issue/k8s/node-current-cluster-insert-benchmark.yaml +``` + +Watch logs: + +```sh +kubectl -n spanner-ns logs -f job/issue-insert-go +kubectl -n spanner-ns logs -f job/issue-insert-node +kubectl -n spanner-ns logs -f job/issue-insert-node-current +kubectl -n spanner-ns logs -f job/issue-insert-node-cluster +kubectl -n spanner-ns logs -f job/issue-insert-node-current-cluster +``` + +Defaults match the customer repro shape: + +- `INSERT_COUNT=1000` +- `INSERT_CONCURRENCY=110` +- `BATCH_COUNT=1` +- 3 CPU request/limit + +Node cluster job uses `CLUSTER_WORKERS=3`. It splits total work across workers: + +- total batches stays `1000` +- total concurrency stays about `110` (`37 + 37 + 36`) +- each worker has a separate Node event loop and Spanner client + +Use `VERBOSE_BATCH_LOGS=false` to remove per-batch logging overhead. diff --git a/handwritten/spanner/issue/build-images.sh b/handwritten/spanner/issue/build-images.sh new file mode 100755 index 00000000000..ec8abc7e9a3 --- /dev/null +++ b/handwritten/spanner/issue/build-images.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +IMAGE_REPO="${IMAGE_REPO:-us-central1-docker.pkg.dev/span-cloud-testing/irahul-images}" +SPANNER_VERSION="${SPANNER_VERSION:-8.6.0}" +PUSH="${PUSH:-true}" +PLATFORM="${PLATFORM:-linux/amd64}" +BUILD_CURRENT="${BUILD_CURRENT:-true}" + +docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" -t "$IMAGE_REPO/issue-insert-go:latest" "$ROOT" + +docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ + --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ + -t "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" \ + -t "$IMAGE_REPO/issue-insert-node:latest" \ + "$ROOT" + +if [[ "$BUILD_CURRENT" == "true" ]]; then + docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ + -t "$IMAGE_REPO/issue-insert-node:current" \ + "$ROOT" + docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster-current" \ + -t "$IMAGE_REPO/issue-insert-node-cluster:current" \ + -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ + "$ROOT" +else + docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ + --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ + -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ + -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ + "$ROOT" +fi + +if [[ "$PUSH" == "true" ]]; then + docker push "$IMAGE_REPO/issue-insert-go:latest" + docker push "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" + docker push "$IMAGE_REPO/issue-insert-node:latest" + if [[ "$BUILD_CURRENT" == "true" ]]; then + docker push "$IMAGE_REPO/issue-insert-node:current" + docker push "$IMAGE_REPO/issue-insert-node-cluster:current" + docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" + else + docker push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" + docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" + fi +fi diff --git a/handwritten/spanner/issue/go/go.mod b/handwritten/spanner/issue/go/go.mod new file mode 100644 index 00000000000..5e8c9c1f0b4 --- /dev/null +++ b/handwritten/spanner/issue/go/go.mod @@ -0,0 +1,55 @@ +module spanner-insert-benchmark + +go 1.25.0 + +require ( + cloud.google.com/go/spanner v1.91.0 + google.golang.org/api v0.278.0 + google.golang.org/grpc v1.81.0 +) + +require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/monitoring v1.25.0 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/handwritten/spanner/issue/go/go.sum b/handwritten/spanner/issue/go/go.sum new file mode 100644 index 00000000000..c894b164586 --- /dev/null +++ b/handwritten/spanner/issue/go/go.sum @@ -0,0 +1,208 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= +cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= +cloud.google.com/go/spanner v1.91.0 h1:XwXfcZ0kc1NT9Uu2IsThFiWtYptB+WgLn/KZEZcyzRg= +cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= +google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handwritten/spanner/issue/main.go b/handwritten/spanner/issue/go/main.go similarity index 100% rename from handwritten/spanner/issue/main.go rename to handwritten/spanner/issue/go/main.go diff --git a/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml new file mode 100644 index 00000000000..f010c017e92 --- /dev/null +++ b/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml @@ -0,0 +1,43 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-insert-go +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-insert-go + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-go:latest + imagePullPolicy: Always + resources: + requests: + cpu: "3" + memory: "2Gi" + limits: + cpu: "3" + memory: "2Gi" + env: + - name: SAMPLE_SIZE + value: "10000" + - name: INSERT_COUNT + value: "1000" + - name: INSERT_CONCURRENCY + value: "110" + - name: BATCH_COUNT + value: "1" + - name: DUPLICATE_INSERT + value: "false" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "irahul-load-test" + - name: DB_DATABASE + value: "db" + - name: DB_SCHEMA + value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml new file mode 100644 index 00000000000..4d5e249247b --- /dev/null +++ b/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml @@ -0,0 +1,47 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-insert-node-cluster +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-insert-node-cluster + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node-cluster:release-8.6.0 + imagePullPolicy: Always + resources: + requests: + cpu: "3" + memory: "3Gi" + limits: + cpu: "3" + memory: "3Gi" + env: + - name: CLUSTER_WORKERS + value: "3" + - name: SAMPLE_SIZE + value: "10000" + - name: INSERT_COUNT + value: "1000" + - name: INSERT_CONCURRENCY + value: "24" + - name: BATCH_COUNT + value: "1" + - name: DUPLICATE_INSERT + value: "false" + - name: VERBOSE_BATCH_LOGS + value: "true" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "irahul-load-test" + - name: DB_DATABASE + value: "db" + - name: DB_SCHEMA + value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml new file mode 100644 index 00000000000..7c1d8b60025 --- /dev/null +++ b/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml @@ -0,0 +1,47 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-insert-node-current-cluster +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-insert-node-current-cluster + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node-cluster:current + imagePullPolicy: Always + resources: + requests: + cpu: "3" + memory: "3Gi" + limits: + cpu: "3" + memory: "3Gi" + env: + - name: CLUSTER_WORKERS + value: "3" + - name: SAMPLE_SIZE + value: "10000" + - name: INSERT_COUNT + value: "1000" + - name: INSERT_CONCURRENCY + value: "24" + - name: BATCH_COUNT + value: "1" + - name: DUPLICATE_INSERT + value: "false" + - name: VERBOSE_BATCH_LOGS + value: "true" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "irahul-load-test" + - name: DB_DATABASE + value: "db" + - name: DB_SCHEMA + value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml new file mode 100644 index 00000000000..c8bfd27e8f0 --- /dev/null +++ b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml @@ -0,0 +1,49 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-insert-node-current +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-insert-node-current + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node:current + imagePullPolicy: Always + resources: + requests: + cpu: "3" + memory: "2Gi" + limits: + cpu: "3" + memory: "2Gi" + env: + - name: SAMPLE_SIZE + value: "10000" + - name: INSERT_COUNT + value: "1000" + - name: INSERT_CONCURRENCY + value: "110" + - name: BATCH_COUNT + value: "1" + - name: DUPLICATE_INSERT + value: "false" + - name: VERBOSE_BATCH_LOGS + value: "true" + - name: SPANNER_DISABLE_BUILT_IN_METRICS + value: "true" + - name: SPANNER_DISABLE_BUILTIN_METRICS + value: "true" + - name: DB_PROJECT_ID + value: "emulator" + - name: DB_INSTANCE + value: "device-tracking" + - name: DB_DATABASE + value: "device-tracking" + - name: DB_SCHEMA + value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml new file mode 100644 index 00000000000..8772c009abf --- /dev/null +++ b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml @@ -0,0 +1,45 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-insert-node +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-insert-node + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node:release-8.6.0 + imagePullPolicy: Always + resources: + requests: + cpu: "3" + memory: "2Gi" + limits: + cpu: "3" + memory: "2Gi" + env: + - name: SAMPLE_SIZE + value: "10000" + - name: INSERT_COUNT + value: "1000" + - name: INSERT_CONCURRENCY + value: "110" + - name: BATCH_COUNT + value: "1" + - name: DUPLICATE_INSERT + value: "false" + - name: VERBOSE_BATCH_LOGS + value: "true" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "irahul-load-test" + - name: DB_DATABASE + value: "db" + - name: DB_SCHEMA + value: "tracking" diff --git a/handwritten/spanner/issue/node/benchmark.js b/handwritten/spanner/issue/node/benchmark.js new file mode 100644 index 00000000000..9e4c99dceed --- /dev/null +++ b/handwritten/spanner/issue/node/benchmark.js @@ -0,0 +1,348 @@ +'use strict'; + +const {performance} = require('perf_hooks'); +const {randomUUID, webcrypto} = require('crypto'); +const {Spanner} = require('@google-cloud/spanner'); +const {status: Status} = require('@grpc/grpc-js'); + +const subtle = webcrypto.subtle; + +const SAMPLE_SIZE = envInt('SAMPLE_SIZE', 10000); +const TOTAL_INSERT_COUNT = envInt('INSERT_COUNT', 1000); +const TOTAL_INSERT_CONCURRENCY = envInt('INSERT_CONCURRENCY', 110); +const BATCH_COUNT = envInt('BATCH_COUNT', 1); +const DUPLICATE_INSERT = process.env.DUPLICATE_INSERT === 'true'; +const VERBOSE_BATCH_LOGS = envBool('VERBOSE_BATCH_LOGS', true); + +const DB_PROJECT_ID = process.env.DB_PROJECT_ID || 'emulator'; +const DB_INSTANCE = process.env.DB_INSTANCE || 'device-tracking'; +const DB_DATABASE = process.env.DB_DATABASE || 'device-tracking'; +const DB_SCHEMA = process.env.DB_SCHEMA || 'tracking'; +const SPANNER_NUM_CHANNELS = envInt('SPANNER_NUM_CHANNELS', 0); +const DISABLE_BUILT_IN_METRICS = envBool('SPANNER_DISABLE_BUILT_IN_METRICS', true); +const POOL_OPTIONS = process.env.POOL_OPTIONS + ? JSON.parse(process.env.POOL_OPTIONS) + : { + max: 20, + min: 1, + incStep: 5, + maxIdle: 1, + idlesAfter: 1, + keepAlive: 10, + acquireTimeout: 10_000, + fail: false, + }; + +async function runBenchmark(options = {}) { + const workerIndex = options.workerIndex ?? envInt('WORKER_INDEX', 0); + const workerCount = options.workerCount ?? envInt('WORKER_COUNT', 1); + const partition = partitionRange(TOTAL_INSERT_COUNT, workerIndex, workerCount); + const concurrency = partitionCount(TOTAL_INSERT_CONCURRENCY, workerIndex, workerCount); + const insertCount = partition.count; + + console.log('Initializing database connection...'); + console.log( + `Configuration: SAMPLE_SIZE=${SAMPLE_SIZE}, INSERT_COUNT=${TOTAL_INSERT_COUNT}, ` + + `INSERT_CONCURRENCY=${TOTAL_INSERT_CONCURRENCY}, BATCH_COUNT=${BATCH_COUNT}, ` + + `DUPLICATE_INSERT=${DUPLICATE_INSERT}, WORKER_INDEX=${workerIndex}, ` + + `WORKER_COUNT=${workerCount}, WORKER_INSERT_COUNT=${insertCount}, ` + + `WORKER_CONCURRENCY=${concurrency}`, + ); + + const spannerOptions = { + projectId: DB_PROJECT_ID, + disableBuiltInMetrics: DISABLE_BUILT_IN_METRICS, + }; + if (SPANNER_NUM_CHANNELS > 0) spannerOptions.numChannels = SPANNER_NUM_CHANNELS; + + const spanner = new Spanner(spannerOptions); + const instance = spanner.instance(DB_INSTANCE); + const poolOpts = + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS === 'false' + ? POOL_OPTIONS + : {}; + const database = instance.database(DB_DATABASE, poolOpts); + const table = database.table(`${DB_SCHEMA}.DeviceRecentActivityLog`); + + try { + console.log('Generating benchmark data...'); + console.log(` Will generate IDs with cardinality: ${SAMPLE_SIZE}`); + + const numDevices = SAMPLE_SIZE; + const numDetails = SAMPLE_SIZE; + const numRequests = SAMPLE_SIZE; + const numLocations = SAMPLE_SIZE; + + const totalRecords = insertCount * BATCH_COUNT; + const totalInserts = DUPLICATE_INSERT ? totalRecords * 2 : totalRecords; + console.log(`\nCreating ${totalRecords} records in ${insertCount} batches of ${BATCH_COUNT}...\n`); + console.log( + DUPLICATE_INSERT + ? `Total insert operations: ${totalInserts} (${totalRecords} + ${totalRecords} duplicates)\n` + : `Total insert operations: ${totalInserts} (no duplicates)\n`, + ); + + const batches = []; + for (let i = 0; i < insertCount; i++) { + const batch = []; + const globalBatchIndex = partition.start + i; + for (let j = 0; j < BATCH_COUNT; j++) { + batch.push( + await createBenchmarkRecord({ + index: globalBatchIndex * BATCH_COUNT + j, + numDevices, + numDetails, + numRequests, + numLocations, + }), + ); + } + batches.push(batch); + } + + console.log(`\nStarting ${insertCount} batch inserts with concurrency: ${concurrency}...\n`); + + const durations = new Array(insertCount); + let alreadyExistsCount = 0; + const overallStartTime = performance.now(); + + await asyncMap( + batches, + async (batch, i) => { + const globalBatchIndex = partition.start + i; + const label = `worker-${workerIndex} batch-${globalBatchIndex} (${BATCH_COUNT} records, concurrency: ${concurrency})`; + const startTime = performance.now(); + if (VERBOSE_BATCH_LOGS) console.log(label); + + try { + await table.insert(batch); + } catch (e) { + if (e.code === Status.ALREADY_EXISTS) { + alreadyExistsCount++; + if (VERBOSE_BATCH_LOGS) console.log(` ${label} - ALREADY_EXISTS (expected on retry)`); + } else { + throw e; + } + } + + if (DUPLICATE_INSERT) { + try { + await table.insert(batch); + } catch (e) { + if (e.code === Status.ALREADY_EXISTS) { + alreadyExistsCount++; + if (VERBOSE_BATCH_LOGS) console.log(` ${label} - ALREADY_EXISTS (expected on duplicate)`); + } else { + throw e; + } + } + } + + const duration = performance.now() - startTime; + durations[i] = duration; + + if (VERBOSE_BATCH_LOGS && (i + 1) % 10 === 0) { + const last = durations.slice(Math.max(0, i - 9), i + 1).filter(Number.isFinite); + const avg = last.reduce((a, b) => a + b, 0) / last.length; + console.log( + ` [${i + 1}/${insertCount}] Last ${last.length} batches avg: ${avg.toFixed(2)}ms ` + + `(${BATCH_COUNT} records/batch, ${concurrency} concurrent)\n`, + ); + } + }, + {concurrency}, + ); + + const overallDuration = performance.now() - overallStartTime; + const summary = buildSummary({ + workerIndex, + workerCount, + insertCount, + batchCount: BATCH_COUNT, + totalRecords, + totalInserts, + alreadyExistsCount, + concurrency, + overallDuration, + durations, + }); + printSummary(summary); + return summary; + } finally { + await database.close(); + spanner.close(); + } +} + +async function sha256Hash(str) { + const encoder = new TextEncoder(); + return Array.from(new Uint8Array(await subtle.digest('SHA-256', encoder.encode(str)))) + .map(b => `00${b.toString(16)}`.slice(-2)) + .join(''); +} + +async function createDeviceRecentActivityLogId(opts) { + return sha256Hash( + `${opts.deviceId}${opts.deviceDetailsId}${opts.httpRequestDetailsId}${opts.createdAt.getTime()}${opts.sessionId ?? ''}`, + ); +} + +function ipToBytes(ipAddress) { + const octets = ipAddress.split('.'); + if (octets.length !== 4) throw new Error('Invalid IP address format'); + const bytes = Buffer.alloc(4); + for (let i = 0; i < 4; i++) bytes[i] = parseInt(octets[i], 10); + return bytes; +} + +async function asyncMap(items, mapperFn, opts = {}) { + const concurrency = Math.max(1, opts.concurrency ?? Infinity); + const results = new Array(items.length); + let currentIndex = 0; + + async function processNext() { + while (currentIndex < items.length) { + const index = currentIndex++; + results[index] = await mapperFn(items[index], index); + } + } + + const workers = Array.from({length: Math.min(concurrency, items.length)}, () => processNext()); + await Promise.all(workers); + return results; +} + +async function createBenchmarkRecord(opts) { + const deviceRecordId = await sha256Hash(`device-${opts.index % opts.numDevices}`); + const deviceDetailsId = await sha256Hash(`detail-${opts.index % opts.numDetails}`); + const httpRequestDetailsId = await sha256Hash(`request-${opts.index % opts.numRequests}`); + const httpRequestLocationId = + opts.numLocations > 0 ? await sha256Hash(`location-${opts.index % opts.numLocations}`) : null; + const xRequestId = randomUUID(); + const createdAt = new Date(Date.now() + opts.index * 1000); + const sessionId = randomUUID(); + const deviceRecentActivityLogId = await createDeviceRecentActivityLogId({ + deviceId: `benchmark-${opts.index}`, + deviceDetailsId, + httpRequestDetailsId, + createdAt, + sessionId, + }); + + return { + deviceRecentActivityLogId, + deviceRecordId, + deviceDetailsId, + httpRequestDetailsId, + ipAddress: ipToBytes('192.168.1.100'), + institutionId: 'benchmark-institution', + userId: Math.random() > 0.5 ? 'user-123' : null, + username: null, + xRequestId, + httpRequestLocationId, + latency: Math.random() > 0.5 ? Math.floor(Math.random() * 100) : null, + sessionId, + createdAt, + }; +} + +function buildSummary(input) { + const durations = input.durations.filter(Number.isFinite); + const sorted = [...durations].sort((a, b) => a - b); + const sum = durations.reduce((a, b) => a + b, 0); + return { + workerIndex: input.workerIndex, + workerCount: input.workerCount, + totalBatches: input.insertCount, + batchSize: input.batchCount, + totalRecords: input.totalRecords, + totalInserts: input.totalInserts, + alreadyExistsCount: input.alreadyExistsCount, + concurrency: input.concurrency, + totalTimeMs: input.overallDuration, + insertsPerSecond: input.totalInserts / (input.overallDuration / 1000), + avgBatchDurationMs: durations.length ? sum / durations.length : 0, + minBatchDurationMs: durations.length ? Math.min(...durations) : 0, + maxBatchDurationMs: durations.length ? Math.max(...durations) : 0, + p50Ms: percentile(sorted, 0.5), + p90Ms: percentile(sorted, 0.9), + p95Ms: percentile(sorted, 0.95), + p99Ms: percentile(sorted, 0.99), + durations, + }; +} + +function printSummary(summary, title = 'Benchmark Summary') { + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(` ${title}`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + if (summary.workerCount > 1) { + console.log(` Worker: ${summary.workerIndex}/${summary.workerCount}`); + } + console.log(` Total Batches: ${summary.totalBatches}`); + console.log(` Batch Size: ${summary.batchSize}`); + console.log(` Total Records: ${summary.totalRecords}`); + console.log(` Total Inserts: ${summary.totalInserts} (includes ${summary.alreadyExistsCount} duplicates)`); + console.log(` Concurrency: ${summary.concurrency}`); + console.log(` Total Time: ${summary.totalTimeMs.toFixed(2)}ms`); + console.log(` Inserts/Second: ${summary.insertsPerSecond.toFixed(2)}`); + console.log(` Avg Batch Duration: ${summary.avgBatchDurationMs.toFixed(2)}ms`); + console.log(` Min Batch Duration: ${summary.minBatchDurationMs.toFixed(2)}ms`); + console.log(` Max Batch Duration: ${summary.maxBatchDurationMs.toFixed(2)}ms`); + console.log(` P50: ${summary.p50Ms.toFixed(2)}ms`); + console.log(` P90: ${summary.p90Ms.toFixed(2)}ms`); + console.log(` P95: ${summary.p95Ms.toFixed(2)}ms`); + console.log(` P99: ${summary.p99Ms.toFixed(2)}ms`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); +} + +function aggregateSummaries(summaries, totalWallTimeMs) { + const durations = summaries.flatMap(summary => summary.durations || []); + return buildSummary({ + workerIndex: 0, + workerCount: summaries.length, + insertCount: summaries.reduce((sum, summary) => sum + summary.totalBatches, 0), + batchCount: summaries[0]?.batchSize || BATCH_COUNT, + totalRecords: summaries.reduce((sum, summary) => sum + summary.totalRecords, 0), + totalInserts: summaries.reduce((sum, summary) => sum + summary.totalInserts, 0), + alreadyExistsCount: summaries.reduce((sum, summary) => sum + summary.alreadyExistsCount, 0), + concurrency: summaries.reduce((sum, summary) => sum + summary.concurrency, 0), + overallDuration: totalWallTimeMs, + durations, + }); +} + +function percentile(sorted, fraction) { + if (!sorted.length) return 0; + const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction)); + return sorted[index]; +} + +function partitionRange(total, index, count) { + const start = Math.floor((total * index) / count); + const end = Math.floor((total * (index + 1)) / count); + return {start, end, count: end - start}; +} + +function partitionCount(total, index, count) { + return partitionRange(total, index, count).count; +} + +function envInt(key, fallback) { + const value = process.env[key]; + return value === undefined || value === '' ? fallback : parseInt(value, 10); +} + +function envBool(key, fallback) { + const value = process.env[key]; + return value === undefined || value === '' ? fallback : value.toLowerCase() === 'true'; +} + +if (require.main === module) { + runBenchmark().catch(err => { + console.error(err); + process.exit(1); + }); +} + +module.exports = {aggregateSummaries, printSummary, runBenchmark}; diff --git a/handwritten/spanner/issue/node/cluster-runner.js b/handwritten/spanner/issue/node/cluster-runner.js new file mode 100644 index 00000000000..ac5ad046b34 --- /dev/null +++ b/handwritten/spanner/issue/node/cluster-runner.js @@ -0,0 +1,59 @@ +'use strict'; + +const cluster = require('cluster'); +const os = require('os'); +const {performance} = require('perf_hooks'); +const {aggregateSummaries, printSummary, runBenchmark} = require('./benchmark'); + +const workerCount = parseInt(process.env.CLUSTER_WORKERS || process.env.PM2_INSTANCES || '3', 10) || os.availableParallelism?.() || os.cpus().length; + +if (cluster.isPrimary) { + const start = performance.now(); + const summaries = []; + let failed = false; + + console.log(`Starting node cluster benchmark with ${workerCount} workers`); + for (let i = 0; i < workerCount; i++) { + const worker = cluster.fork({ + ...process.env, + WORKER_INDEX: String(i), + WORKER_COUNT: String(workerCount), + CLUSTER_CHILD: 'true', + }); + worker.on('message', message => { + if (message?.type === 'summary') summaries.push(message.summary); + if (message?.type === 'error') { + failed = true; + console.error(`worker ${i} failed: ${message.error}`); + } + }); + } + + cluster.on('exit', (worker, code, signal) => { + if (code !== 0) { + failed = true; + console.error(`worker ${worker.id} exited with code=${code} signal=${signal}`); + } + if (Object.keys(cluster.workers).length === 0) { + const totalWallTimeMs = performance.now() - start; + if (summaries.length) { + const aggregate = aggregateSummaries(summaries, totalWallTimeMs); + printSummary(aggregate, 'Combined Node Cluster Benchmark Summary'); + } + process.exit(failed ? 1 : 0); + } + }); +} else { + runBenchmark({ + workerIndex: parseInt(process.env.WORKER_INDEX || '0', 10), + workerCount: parseInt(process.env.WORKER_COUNT || '1', 10), + }) + .then(summary => { + if (process.send) process.send({type: 'summary', summary}); + }) + .catch(err => { + if (process.send) process.send({type: 'error', error: err.stack || err.message}); + console.error(err); + process.exit(1); + }); +} From be6dd4aeef21d02f1f9db6eca4b3e6b8a6d64248 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 6 May 2026 14:11:36 +0530 Subject: [PATCH 3/7] add sample seed data --- handwritten/spanner/issue/README.md | 37 ++++++++++++ .../spanner/issue/create-reference.sql | 23 ++++++++ .../spanner/issue/seed-reference-data.js | 56 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 handwritten/spanner/issue/create-reference.sql create mode 100644 handwritten/spanner/issue/seed-reference-data.js diff --git a/handwritten/spanner/issue/README.md b/handwritten/spanner/issue/README.md index f11e1ea8fe7..f8e5a819f1d 100644 --- a/handwritten/spanner/issue/README.md +++ b/handwritten/spanner/issue/README.md @@ -45,3 +45,40 @@ Node cluster job uses `CLUSTER_WORKERS=3`. It splits total work across workers: - each worker has a separate Node event loop and Spanner client Use `VERBOSE_BATCH_LOGS=false` to remove per-batch logging overhead. + +## Reference table setup for Go benchmark + +`issue/go/main.go` samples IDs from these tables before inserting into `DeviceRecentActivityLog`: + +- `tracking.Devices(deviceRecordId)` +- `tracking.DeviceDetails(deviceDetailsId)` +- `tracking.HttpRequestDetails(httpRequestDetailsId)` +- `tracking.HttpRequestLocations(httpRequestLocationId)` + +The insert target table does not require these rows unless you add foreign keys. They are only needed by the Go benchmark script. Minimum to run: 1 row in the first 3 tables; locations can be empty. Recommended for default `SAMPLE_SIZE=10000`: seed 10000 rows in each table. + +DDL: + +```sh +gcloud spanner databases ddl update db \ + --instance=irahul-load-test \ + --project=span-cloud-testing \ + --ddl-file=issue/create-reference.sql +``` + +Seed 10000 rows per reference table: + +```sh +node issue/seed-reference-data.js +``` + +Use env overrides if needed: + +```sh +DB_PROJECT_ID=span-cloud-testing \ +DB_INSTANCE=irahul-load-test \ +DB_DATABASE=db \ +DB_SCHEMA=tracking \ +SAMPLE_SIZE=10000 \ +node issue/seed-reference-data.js +``` diff --git a/handwritten/spanner/issue/create-reference.sql b/handwritten/spanner/issue/create-reference.sql new file mode 100644 index 00000000000..e9931e57db6 --- /dev/null +++ b/handwritten/spanner/issue/create-reference.sql @@ -0,0 +1,23 @@ +-- Minimal reference tables needed by issue/go/main.go. +-- DeviceRecentActivityLog insert itself has no foreign keys in issue/create.sql; +-- these tables only feed benchmark ID sampling. + +CREATE TABLE tracking.Devices ( + deviceRecordId STRING(64) NOT NULL, + createdAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true) +) PRIMARY KEY (deviceRecordId); + +CREATE TABLE tracking.DeviceDetails ( + deviceDetailsId STRING(64) NOT NULL, + createdAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true) +) PRIMARY KEY (deviceDetailsId); + +CREATE TABLE tracking.HttpRequestDetails ( + httpRequestDetailsId STRING(64) NOT NULL, + createdAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true) +) PRIMARY KEY (httpRequestDetailsId); + +CREATE TABLE tracking.HttpRequestLocations ( + httpRequestLocationId STRING(64) NOT NULL, + createdAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true) +) PRIMARY KEY (httpRequestLocationId); diff --git a/handwritten/spanner/issue/seed-reference-data.js b/handwritten/spanner/issue/seed-reference-data.js new file mode 100644 index 00000000000..65cc842ccf6 --- /dev/null +++ b/handwritten/spanner/issue/seed-reference-data.js @@ -0,0 +1,56 @@ +'use strict'; + +const crypto = require('crypto'); +const {Spanner} = require('@google-cloud/spanner'); + +const SAMPLE_SIZE = envInt('SAMPLE_SIZE', 10000); +const DB_PROJECT_ID = process.env.DB_PROJECT_ID || 'span-cloud-testing'; +const DB_INSTANCE = process.env.DB_INSTANCE || 'irahul-load-test'; +const DB_DATABASE = process.env.DB_DATABASE || 'db'; +const DB_SCHEMA = process.env.DB_SCHEMA || 'tracking'; +const BATCH_SIZE = envInt('SEED_BATCH_SIZE', 500); + +async function main() { + const spanner = new Spanner({projectId: DB_PROJECT_ID, disableBuiltInMetrics: true}); + const database = spanner.instance(DB_INSTANCE).database(DB_DATABASE); + try { + await seedTable(database, `${DB_SCHEMA}.Devices`, 'deviceRecordId', 'device'); + await seedTable(database, `${DB_SCHEMA}.DeviceDetails`, 'deviceDetailsId', 'detail'); + await seedTable(database, `${DB_SCHEMA}.HttpRequestDetails`, 'httpRequestDetailsId', 'request'); + await seedTable(database, `${DB_SCHEMA}.HttpRequestLocations`, 'httpRequestLocationId', 'location'); + } finally { + await database.close(); + spanner.close(); + } +} + +async function seedTable(database, tableName, idColumn, prefix) { + const table = database.table(tableName); + console.log(`Seeding ${tableName}: ${SAMPLE_SIZE} rows`); + for (let offset = 0; offset < SAMPLE_SIZE; offset += BATCH_SIZE) { + const rows = []; + const end = Math.min(SAMPLE_SIZE, offset + BATCH_SIZE); + for (let i = offset; i < end; i++) { + rows.push({ + [idColumn]: sha256(`${prefix}-${i}`), + createdAt: 'spanner.commit_timestamp()', + }); + } + await table.upsert(rows); + console.log(` ${tableName}: ${end}/${SAMPLE_SIZE}`); + } +} + +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex'); +} + +function envInt(key, fallback) { + const value = process.env[key]; + return value === undefined || value === '' ? fallback : parseInt(value, 10); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); From e8dff39fb9c1730938dc8e742193bb00386adbf9 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 6 May 2026 16:00:06 +0530 Subject: [PATCH 4/7] fix dockerfiles --- handwritten/spanner/issue/Dockerfile.node | 2 +- handwritten/spanner/issue/Dockerfile.node-cluster | 4 ++-- handwritten/spanner/issue/Dockerfile.node-cluster-current | 4 ++-- handwritten/spanner/issue/Dockerfile.node-current | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/handwritten/spanner/issue/Dockerfile.node b/handwritten/spanner/issue/Dockerfile.node index 3d3bced122e..f123cec10e7 100644 --- a/handwritten/spanner/issue/Dockerfile.node +++ b/handwritten/spanner/issue/Dockerfile.node @@ -5,6 +5,6 @@ ARG SPANNER_VERSION=8.6.0 RUN npm init -y \ && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js tsx \ && npm cache clean --force -COPY issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +COPY --chown=node:node --chmod=0444 issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts USER node ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster b/handwritten/spanner/issue/Dockerfile.node-cluster index 6a26d832692..7658694fa2e 100644 --- a/handwritten/spanner/issue/Dockerfile.node-cluster +++ b/handwritten/spanner/issue/Dockerfile.node-cluster @@ -5,7 +5,7 @@ ARG SPANNER_VERSION=8.6.0 RUN npm init -y \ && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js \ && npm cache clean --force -COPY issue/node/benchmark.js ./benchmark.js -COPY issue/node/cluster-runner.js ./cluster-runner.js +COPY --chown=node:node --chmod=0444 issue/node/benchmark.js ./benchmark.js +COPY --chown=node:node --chmod=0444 issue/node/cluster-runner.js ./cluster-runner.js USER node ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster-current b/handwritten/spanner/issue/Dockerfile.node-cluster-current index 9111021cb65..b8289d99cb2 100644 --- a/handwritten/spanner/issue/Dockerfile.node-cluster-current +++ b/handwritten/spanner/issue/Dockerfile.node-cluster-current @@ -22,7 +22,7 @@ COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tg RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ && npm cache clean --force \ && node -e "console.log(require('@google-cloud/spanner/package.json').version)" -COPY issue/node/benchmark.js ./benchmark.js -COPY issue/node/cluster-runner.js ./cluster-runner.js +COPY --chown=node:node --chmod=0444 issue/node/benchmark.js ./benchmark.js +COPY --chown=node:node --chmod=0444 issue/node/cluster-runner.js ./cluster-runner.js USER node ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-current b/handwritten/spanner/issue/Dockerfile.node-current index d4671ea2b08..4097c096f37 100644 --- a/handwritten/spanner/issue/Dockerfile.node-current +++ b/handwritten/spanner/issue/Dockerfile.node-current @@ -22,6 +22,6 @@ COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tg RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ && npm cache clean --force \ && node -e "console.log(require('@google-cloud/spanner/package.json').version)" -COPY issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +COPY --chown=node:node --chmod=0444 issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts USER node ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] From c90a286c5a20967b8ac5e15a208dd435556e98e7 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 6 May 2026 16:11:36 +0530 Subject: [PATCH 5/7] push and build --- handwritten/spanner/issue/build-images.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/handwritten/spanner/issue/build-images.sh b/handwritten/spanner/issue/build-images.sh index ec8abc7e9a3..947b0fa77bc 100755 --- a/handwritten/spanner/issue/build-images.sh +++ b/handwritten/spanner/issue/build-images.sh @@ -8,7 +8,9 @@ PUSH="${PUSH:-true}" PLATFORM="${PLATFORM:-linux/amd64}" BUILD_CURRENT="${BUILD_CURRENT:-true}" -docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" -t "$IMAGE_REPO/issue-insert-go:latest" "$ROOT" +docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" \ + -t "$IMAGE_REPO/issue-insert-go:latest" \ + "$ROOT" docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ @@ -16,6 +18,11 @@ docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ -t "$IMAGE_REPO/issue-insert-node:latest" \ "$ROOT" +docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ + --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ + -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ + "$ROOT" + if [[ "$BUILD_CURRENT" == "true" ]]; then docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ -t "$IMAGE_REPO/issue-insert-node:current" \ @@ -25,23 +32,20 @@ if [[ "$BUILD_CURRENT" == "true" ]]; then -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ "$ROOT" else - docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ - --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ - -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ - -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ - "$ROOT" + docker tag "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ + "$IMAGE_REPO/issue-insert-node-cluster:latest" fi if [[ "$PUSH" == "true" ]]; then docker push "$IMAGE_REPO/issue-insert-go:latest" docker push "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" docker push "$IMAGE_REPO/issue-insert-node:latest" + docker push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" if [[ "$BUILD_CURRENT" == "true" ]]; then docker push "$IMAGE_REPO/issue-insert-node:current" docker push "$IMAGE_REPO/issue-insert-node-cluster:current" docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" else - docker push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" fi fi From 14dfabea91bf869a7c6726659a4bc449c4ba0c2a Mon Sep 17 00:00:00 2001 From: Surbhi Garg Date: Fri, 8 May 2026 16:30:17 +0530 Subject: [PATCH 6/7] benchmark test --- handwritten/spanner/issue/Dockerfile.go-raw | 11 + handwritten/spanner/issue/Dockerfile.node | 2 +- .../spanner/issue/Dockerfile.node-cluster | 2 +- handwritten/spanner/issue/Dockerfile.node-raw | 26 + handwritten/spanner/issue/README.md | 17 +- .../spanner/issue/benchmark-insert-repl.ts | 2 +- handwritten/spanner/issue/build-images.sh | 75 +-- handwritten/spanner/issue/go/main.go | 3 +- .../spanner/issue/go/raw_grpc_benchmark.go | 484 ++++++++++++++++++ .../issue/k8s/go-insert-benchmark.yaml | 10 +- .../k8s/node-cluster-insert-benchmark.yaml | 12 +- ...node-current-cluster-insert-benchmark.yaml | 6 +- .../k8s/node-current-insert-benchmark.yaml | 2 +- .../issue/k8s/node-insert-benchmark.yaml | 12 +- .../spanner/issue/k8s/raw-grpc-benchmark.yaml | 75 +++ handwritten/spanner/issue/node/package.json | 10 + .../spanner/issue/node/raw_grpc_benchmark.js | 345 +++++++++++++ .../issue/{ => node}/seed-reference-data.js | 8 +- handwritten/spanner/src/instrument.ts | 4 + 19 files changed, 1040 insertions(+), 66 deletions(-) create mode 100644 handwritten/spanner/issue/Dockerfile.go-raw create mode 100644 handwritten/spanner/issue/Dockerfile.node-raw create mode 100644 handwritten/spanner/issue/go/raw_grpc_benchmark.go create mode 100644 handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml create mode 100644 handwritten/spanner/issue/node/package.json create mode 100644 handwritten/spanner/issue/node/raw_grpc_benchmark.js rename handwritten/spanner/issue/{ => node}/seed-reference-data.js (86%) diff --git a/handwritten/spanner/issue/Dockerfile.go-raw b/handwritten/spanner/issue/Dockerfile.go-raw new file mode 100644 index 00000000000..216eea87530 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.go-raw @@ -0,0 +1,11 @@ +FROM golang:1.25-bookworm AS build +WORKDIR /src +COPY issue/go/go.mod ./go.mod +COPY issue/go/go.sum ./go.sum +RUN go mod download +COPY issue/go/raw_grpc_benchmark.go ./raw_grpc_benchmark.go +RUN CGO_ENABLED=0 GOOS=linux go build -o /out/raw-grpc-benchmark ./raw_grpc_benchmark.go + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /out/raw-grpc-benchmark /raw-grpc-benchmark +ENTRYPOINT ["/raw-grpc-benchmark"] diff --git a/handwritten/spanner/issue/Dockerfile.node b/handwritten/spanner/issue/Dockerfile.node index f123cec10e7..02acf19fa0a 100644 --- a/handwritten/spanner/issue/Dockerfile.node +++ b/handwritten/spanner/issue/Dockerfile.node @@ -1,7 +1,7 @@ FROM node:22-bookworm-slim ENV NODE_ENV=production WORKDIR /app -ARG SPANNER_VERSION=8.6.0 +ARG SPANNER_VERSION=8.7.1 RUN npm init -y \ && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js tsx \ && npm cache clean --force diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster b/handwritten/spanner/issue/Dockerfile.node-cluster index 7658694fa2e..f9fb8614555 100644 --- a/handwritten/spanner/issue/Dockerfile.node-cluster +++ b/handwritten/spanner/issue/Dockerfile.node-cluster @@ -1,7 +1,7 @@ FROM node:22-bookworm-slim ENV NODE_ENV=production WORKDIR /app -ARG SPANNER_VERSION=8.6.0 +ARG SPANNER_VERSION=8.7.1 RUN npm init -y \ && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js \ && npm cache clean --force diff --git a/handwritten/spanner/issue/Dockerfile.node-raw b/handwritten/spanner/issue/Dockerfile.node-raw new file mode 100644 index 00000000000..5f3e00d8421 --- /dev/null +++ b/handwritten/spanner/issue/Dockerfile.node-raw @@ -0,0 +1,26 @@ +FROM node:22-bookworm-slim AS local-lib +WORKDIR /workspace/spanner +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then \ + npm ci --ignore-scripts; \ + else \ + npm install --ignore-scripts; \ + fi +COPY . . +RUN npm run compile \ + && mkdir -p /tmp/spanner-pack \ + && npm pack --pack-destination /tmp/spanner-pack \ + && cp /tmp/spanner-pack/google-cloud-spanner-*.tgz /tmp/google-cloud-spanner.tgz + +FROM node:22-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +RUN npm init -y >/dev/null 2>&1 || true \ + && npm install --omit=dev @grpc/grpc-js \ + && npm cache clean --force +COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tgz +RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ + && npm cache clean --force +COPY --chown=node:node --chmod=0444 issue/node/raw_grpc_benchmark.js ./raw_grpc_benchmark.js +USER node +ENTRYPOINT ["node", "/app/raw_grpc_benchmark.js"] diff --git a/handwritten/spanner/issue/README.md b/handwritten/spanner/issue/README.md index f8e5a819f1d..a735b6dbaf1 100644 --- a/handwritten/spanner/issue/README.md +++ b/handwritten/spanner/issue/README.md @@ -3,14 +3,19 @@ Build and push all three images: ```sh -IMAGE_REPO=us-central1-docker.pkg.dev/span-cloud-testing/irahul-images \ -SPANNER_VERSION=8.6.0 \ +IMAGE_REPO=us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images \ +SPANNER_VERSION=8.7.1 \ BUILD_CURRENT=true \ ./issue/build-images.sh ``` Run jobs: +Connect to Kubernetes cluster +```sh +gcloud container clusters get-credentials cluster-1 --region us-central1 --project span-cloud-testing +``` + Release baseline uses `issue-insert-node:latest` from npm `SPANNER_VERSION`. Current branch uses `issue-insert-node:current`. Release cluster uses `issue-insert-node-cluster:release-8.6.0`; current cluster uses `issue-insert-node-cluster:current`. ```sh @@ -60,8 +65,8 @@ The insert target table does not require these rows unless you add foreign keys. DDL: ```sh -gcloud spanner databases ddl update db \ - --instance=irahul-load-test \ +gcloud spanner databases ddl update jack_henry_db \ + --instance=gargsurbhi-testing1 \ --project=span-cloud-testing \ --ddl-file=issue/create-reference.sql ``` @@ -76,8 +81,8 @@ Use env overrides if needed: ```sh DB_PROJECT_ID=span-cloud-testing \ -DB_INSTANCE=irahul-load-test \ -DB_DATABASE=db \ +DB_INSTANCE=gargsurbhi-testing1 \ +DB_DATABASE=jack_henry_db \ DB_SCHEMA=tracking \ SAMPLE_SIZE=10000 \ node issue/seed-reference-data.js diff --git a/handwritten/spanner/issue/benchmark-insert-repl.ts b/handwritten/spanner/issue/benchmark-insert-repl.ts index 8f0869cbe20..fd2b8425be9 100644 --- a/handwritten/spanner/issue/benchmark-insert-repl.ts +++ b/handwritten/spanner/issue/benchmark-insert-repl.ts @@ -16,7 +16,7 @@ /* eslint-disable no-await-in-loop */ // Disable multiplexed sessions to use traditional session pool -// This avoids "ReleaseError: Unable to release unknown resource" with SDK v8.6.0 +// This avoids "ReleaseError: Unable to release unknown resource" with SDK v8.7.1 // process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false' // Benchmark configuration - can be overridden via environment variables diff --git a/handwritten/spanner/issue/build-images.sh b/handwritten/spanner/issue/build-images.sh index 947b0fa77bc..93ca699c267 100755 --- a/handwritten/spanner/issue/build-images.sh +++ b/handwritten/spanner/issue/build-images.sh @@ -2,50 +2,61 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -IMAGE_REPO="${IMAGE_REPO:-us-central1-docker.pkg.dev/span-cloud-testing/irahul-images}" -SPANNER_VERSION="${SPANNER_VERSION:-8.6.0}" +IMAGE_REPO="${IMAGE_REPO:-us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images}" +SPANNER_VERSION="${SPANNER_VERSION:-8.7.1}" PUSH="${PUSH:-true}" PLATFORM="${PLATFORM:-linux/amd64}" -BUILD_CURRENT="${BUILD_CURRENT:-true}" +BUILD_CURRENT="${BUILD_CURRENT:-false}" +CONTAINER_TOOL="${CONTAINER_TOOL:-podman}" -docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" \ +# Build Node Raw gRPC image +$CONTAINER_TOOL build --platform linux/amd64 -f issue/Dockerfile.node-raw -t $IMAGE_REPO/issue-raw-grpc-node:latest . +# Build Go Raw gRPC image +$CONTAINER_TOOL build --platform linux/amd64 -f issue/Dockerfile.go-raw -t $IMAGE_REPO/issue-raw-grpc-go:latest . + + + +"$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" \ -t "$IMAGE_REPO/issue-insert-go:latest" \ "$ROOT" -docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ +"$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ -t "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" \ -t "$IMAGE_REPO/issue-insert-node:latest" \ "$ROOT" -docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ - --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ - -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ - "$ROOT" +# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ +# --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ +# -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ +# "$ROOT" -if [[ "$BUILD_CURRENT" == "true" ]]; then - docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ - -t "$IMAGE_REPO/issue-insert-node:current" \ - "$ROOT" - docker build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster-current" \ - -t "$IMAGE_REPO/issue-insert-node-cluster:current" \ - -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ - "$ROOT" -else - docker tag "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ - "$IMAGE_REPO/issue-insert-node-cluster:latest" -fi +# if [[ "$BUILD_CURRENT" == "true" ]]; then +# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ +# -t "$IMAGE_REPO/issue-insert-node:current" \ +# "$ROOT" +# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster-current" \ +# -t "$IMAGE_REPO/issue-insert-node-cluster:current" \ +# -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ +# "$ROOT" +# else +# "$CONTAINER_TOOL" tag "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ +# "$IMAGE_REPO/issue-insert-node-cluster:latest" +# fi if [[ "$PUSH" == "true" ]]; then - docker push "$IMAGE_REPO/issue-insert-go:latest" - docker push "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" - docker push "$IMAGE_REPO/issue-insert-node:latest" - docker push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" - if [[ "$BUILD_CURRENT" == "true" ]]; then - docker push "$IMAGE_REPO/issue-insert-node:current" - docker push "$IMAGE_REPO/issue-insert-node-cluster:current" - docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" - else - docker push "$IMAGE_REPO/issue-insert-node-cluster:latest" - fi +# Push Raw gRPC images + $CONTAINER_TOOL push $IMAGE_REPO/issue-raw-grpc-node:latest + $CONTAINER_TOOL push $IMAGE_REPO/issue-raw-grpc-go:latest + "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-go:latest" + "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" + "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:latest" + # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" + # if [[ "$BUILD_CURRENT" == "true" ]]; then + # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:current" + # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:current" + # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:latest" + # else + # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:latest" + # fi fi diff --git a/handwritten/spanner/issue/go/main.go b/handwritten/spanner/issue/go/main.go index fbbaa25dd4c..360f73e5fdc 100644 --- a/handwritten/spanner/issue/go/main.go +++ b/handwritten/spanner/issue/go/main.go @@ -17,6 +17,7 @@ import ( "cloud.google.com/go/spanner" "google.golang.org/api/iterator" + "google.golang.org/api/option" "google.golang.org/grpc/codes" ) @@ -79,7 +80,7 @@ func runBenchmark(ctx context.Context, projectID, instance, database, schema str fmt.Printf("Configuration: SAMPLE_SIZE=%d, INSERT_COUNT=%d, INSERT_CONCURRENCY=%d, BATCH_COUNT=%d, DUPLICATE_INSERT=%t\n", sampleSize, insertCount, insertConcurrency, batchCount, duplicateInsert) dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instance, database) - client, err := spanner.NewClient(ctx, dbPath) + client, err := spanner.NewClient(ctx, dbPath, option.WithGRPCConnectionPool(1)) if err != nil { return fmt.Errorf("failed to create client: %w", err) } diff --git a/handwritten/spanner/issue/go/raw_grpc_benchmark.go b/handwritten/spanner/issue/go/raw_grpc_benchmark.go new file mode 100644 index 00000000000..54f5f36f452 --- /dev/null +++ b/handwritten/spanner/issue/go/raw_grpc_benchmark.go @@ -0,0 +1,484 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "log" + "math" + mathrand "math/rand" + "os" + "sort" + "strconv" + "sync" + "time" + + gapic "cloud.google.com/go/spanner/apiv1" + "google.golang.org/api/option" + spannerpb "google.golang.org/genproto/googleapis/spanner/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + structpb "google.golang.org/protobuf/types/known/structpb" +) + +func main() { + ctx := context.Background() + + // Read benchmark configuration + projectId := getEnv("DB_PROJECT_ID", "emulator") + instanceId := getEnv("DB_INSTANCE", "device-tracking") + databaseId := getEnv("DB_DATABASE", "device-tracking") + tableName := getEnv("TABLE_NAME", "tracking.DeviceRecentActivityLog") + readCount := getEnvInt("READ_COUNT", 1000) + concurrency := getEnvInt("CONCURRENCY", 10) + + // Write benchmark configuration + sampleSize := getEnvInt("SAMPLE_SIZE", 10000) + insertCount := getEnvInt("INSERT_COUNT", 1000) + insertConcurrency := getEnvInt("INSERT_CONCURRENCY", 110) + batchCount := getEnvInt("BATCH_COUNT", 1) + duplicateInsert := getEnv("DUPLICATE_INSERT", "false") == "true" + + databasePath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId) + + fmt.Printf("Target Database: %s\n", databasePath) + fmt.Printf("Target Table: %s\n", tableName) + fmt.Printf("Read Config: READ_COUNT=%d, CONCURRENCY=%d\n", readCount, concurrency) + fmt.Printf("Write Config: SAMPLE_SIZE=%d, INSERT_COUNT=%d, INSERT_CONCURRENCY=%d, BATCH_COUNT=%d, DUPLICATE_INSERT=%t\n", sampleSize, insertCount, insertConcurrency, batchCount, duplicateInsert) + fmt.Println("gRPC Connection Pool Size: 1") + + // Initialize the generated client with a single connection to match Node.js default behavior + client, err := gapic.NewClient(ctx, option.WithGRPCConnectionPool(4)) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + defer client.Close() + + fmt.Println("\nCreating multiplexed session via raw gRPC...") + session, err := client.CreateSession(ctx, &spannerpb.CreateSessionRequest{ + Database: databasePath, + Session: &spannerpb.Session{ + Multiplexed: true, + }, + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + fmt.Printf("Multiplexed Session created: %s\n", session.Name) + + defer func() { + fmt.Println("\nBenchmark finished. Multiplexed session is left to be managed by server.") + }() + + // ========================================== + // 1. READ BENCHMARK + // ========================================== + fmt.Printf("\nStarting READ benchmark with concurrency %d...\n", concurrency) + + readDurations := make([]time.Duration, readCount) + readSem := make(chan struct{}, concurrency) + var readWg sync.WaitGroup + + readOverallStart := time.Now() + + for i := 0; i < readCount; i++ { + readWg.Add(1) + readSem <- struct{}{} // Acquire + + go func(index int) { + defer readWg.Done() + defer func() { <-readSem }() // Release + + randomId := mathrand.Intn(1000000) + 1 + + req := &spannerpb.ExecuteSqlRequest{ + Session: session.Name, + Sql: fmt.Sprintf("SELECT * FROM %s WHERE deviceRecentActivityLogId = @id", tableName), + Params: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "id": { + Kind: &structpb.Value_StringValue{ + StringValue: fmt.Sprintf("%d", randomId), + }, + }, + }, + }, + ParamTypes: map[string]*spannerpb.Type{ + "id": { + Code: spannerpb.TypeCode_STRING, + }, + }, + } + + startTime := time.Now() + _, err := client.ExecuteSql(ctx, req) + duration := time.Since(startTime) + + if err != nil { + log.Printf("Read failed at index %d: %v", index, err) + } else { + readDurations[index] = duration + } + }(i) + } + + readWg.Wait() + readOverallDuration := time.Since(readOverallStart) + readOpsPerSecond := float64(readCount) / readOverallDuration.Seconds() + + var readSum time.Duration + validReadDurations := make([]time.Duration, 0, readCount) + for _, d := range readDurations { + if d > 0 { + readSum += d + validReadDurations = append(validReadDurations, d) + } + } + + var readAvg time.Duration + if len(validReadDurations) > 0 { + readAvg = readSum / time.Duration(len(validReadDurations)) + } + + sort.Slice(validReadDurations, func(i, j int) bool { + return validReadDurations[i] < validReadDurations[j] + }) + + rp50 := percentile(validReadDurations, 0.5) + rp90 := percentile(validReadDurations, 0.9) + rp95 := percentile(validReadDurations, 0.95) + rp99 := percentile(validReadDurations, 0.99) + + fmt.Println("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Println(" Raw gRPC Benchmark Summary (Go - READ)") + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Printf(" Total Reads: %d\n", readCount) + fmt.Printf(" Concurrency: %d\n", concurrency) + fmt.Printf(" Total Time: %v\n", readOverallDuration) + fmt.Printf(" Reads/Second: %.2f\n", readOpsPerSecond) + fmt.Printf(" Avg Latency: %v\n", readAvg) + fmt.Printf(" P50 Latency: %v\n", rp50) + fmt.Printf(" P90 Latency: %v\n", rp90) + fmt.Printf(" P95 Latency: %v\n", rp95) + fmt.Printf(" P99 Latency: %v\n", rp99) + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + + // ========================================== + // 2. SLEEP 5 SECONDS + // ========================================== + fmt.Println("Sleeping for 5 seconds before starting writes...") + time.Sleep(5 * time.Second) + + // ========================================== + // 3. WRITE BENCHMARK + // ========================================== + fmt.Printf("\nStarting WRITE benchmark with concurrency %d...\n", insertConcurrency) + + totalRecords := insertCount * batchCount + totalInserts := totalRecords + if duplicateInsert { + totalInserts = totalRecords * 2 + } + + // Pre-generate batches + type Record struct { + deviceRecentActivityLogId string + deviceRecordId string + deviceDetailsId string + httpRequestDetailsId string + ipAddress string + institutionId string + userId *string + username *string + xRequestId string + httpRequestLocationId *string + latency *int64 + sessionId string + createdAt time.Time + } + + batches := make([][]*Record, insertCount) + for i := 0; i < insertCount; i++ { + batch := make([]*Record, batchCount) + for j := 0; j < batchCount; j++ { + idx := i*batchCount + j + deviceRecordId := sha256Hash(fmt.Sprintf("device-%d", idx%sampleSize)) + deviceDetailsId := sha256Hash(fmt.Sprintf("detail-%d", idx%sampleSize)) + httpRequestDetailsId := sha256Hash(fmt.Sprintf("request-%d", idx%sampleSize)) + httpRequestLocationIdStr := sha256Hash(fmt.Sprintf("location-%d", idx%sampleSize)) + var httpRequestLocationId *string = &httpRequestLocationIdStr + + xRequestId := generateUUID() + sessionId := generateUUID() + createdAt := time.Now().Add(time.Duration(idx) * time.Second) + + idInput := fmt.Sprintf("benchmark-%d%s%s%d%s", idx, deviceDetailsId, httpRequestDetailsId, createdAt.UnixMilli(), sessionId) + deviceRecentActivityLogId := sha256Hash(idInput) + + var userId *string + if mathrand.Float64() > 0.5 { + uid := "user-123" + userId = &uid + } + + var latency *int64 + if mathrand.Float64() > 0.5 { + lat := int64(mathrand.Intn(100)) + latency = &lat + } + + batch[j] = &Record{ + deviceRecentActivityLogId: deviceRecentActivityLogId, + deviceRecordId: deviceRecordId, + deviceDetailsId: deviceDetailsId, + httpRequestDetailsId: httpRequestDetailsId, + ipAddress: base64.StdEncoding.EncodeToString([]byte{192, 168, 1, 100}), + institutionId: "benchmark-institution", + userId: userId, + username: nil, + xRequestId: xRequestId, + httpRequestLocationId: httpRequestLocationId, + latency: latency, + sessionId: sessionId, + createdAt: createdAt, + } + } + batches[i] = batch + } + + writeDurations := make([]float64, insertCount) + var alreadyExistsCount int64 + var writeMu sync.Mutex + + writeSem := make(chan struct{}, insertConcurrency) + var writeWg sync.WaitGroup + writeOverallStart := time.Now() + + for i, batch := range batches { + writeWg.Add(1) + writeSem <- struct{}{} + + go func(index int, records []*Record) { + defer writeWg.Done() + defer func() { <-writeSem }() + + start := time.Now() + + mutations := make([]*spannerpb.Mutation, len(records)) + for j, rec := range records { + values := []*structpb.Value{ + protoValue(rec.deviceRecentActivityLogId), + protoValue(rec.deviceRecordId), + protoValue(rec.deviceDetailsId), + protoValue(rec.httpRequestDetailsId), + protoValue(rec.ipAddress), + protoValue(rec.institutionId), + protoValue(rec.userId), + protoValue(rec.username), + protoValue(rec.xRequestId), + protoValue(rec.httpRequestLocationId), + protoValue(rec.latency), + protoValue(rec.sessionId), + protoValue(rec.createdAt.UTC().Format(time.RFC3339Nano)), + } + + mutations[j] = &spannerpb.Mutation{ + Operation: &spannerpb.Mutation_Insert{ + Insert: &spannerpb.Mutation_Write{ + Table: tableName, + Columns: []string{ + "deviceRecentActivityLogId", + "deviceRecordId", + "deviceDetailsId", + "httpRequestDetailsId", + "ipAddress", + "institutionId", + "userId", + "username", + "xRequestId", + "httpRequestLocationId", + "latency", + "sessionId", + "createdAt", + }, + Values: []*structpb.ListValue{ + {Values: values}, + }, + }, + }, + } + } + + commitReq := &spannerpb.CommitRequest{ + Session: session.Name, + Transaction: &spannerpb.CommitRequest_SingleUseTransaction{ + SingleUseTransaction: &spannerpb.TransactionOptions{ + Mode: &spannerpb.TransactionOptions_ReadWrite_{ + ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, + }, + }, + }, + Mutations: mutations, + } + + _, err := client.Commit(ctx, commitReq) + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists { + writeMu.Lock() + alreadyExistsCount++ + writeMu.Unlock() + } else { + log.Printf("Commit failed at batch %d: %v", index, err) + } + } + + if duplicateInsert { + _, err = client.Commit(ctx, commitReq) + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists { + writeMu.Lock() + alreadyExistsCount++ + writeMu.Unlock() + } else { + log.Printf("Duplicate commit failed at batch %d: %v", index, err) + } + } + } + + duration := time.Since(start).Milliseconds() + writeMu.Lock() + writeDurations[index] = float64(duration) + writeMu.Unlock() + + if (index+1)%100 == 0 || (index+1) == insertCount { + writeMu.Lock() + windowStart := index - 99 + if windowStart < 0 { + windowStart = 0 + } + var sum float64 + count := 0 + for k := windowStart; k <= index; k++ { + sum += writeDurations[k] + count++ + } + avg := sum / float64(count) + writeMu.Unlock() + fmt.Printf(" [%d/%d] Last batches avg: %.2fms (%d records/batch)\n", index+1, insertCount, avg, batchCount) + } + }(i, batch) + } + + writeWg.Wait() + writeOverallDuration := time.Since(writeOverallStart).Milliseconds() + writeInsertsPerSecond := float64(totalInserts) / (float64(writeOverallDuration) / 1000.0) + + var writeSum float64 + writeMin := writeDurations[0] + writeMax := writeDurations[0] + for _, d := range writeDurations { + writeSum += d + if d < writeMin { + writeMin = d + } + if d > writeMax { + writeMax = d + } + } + writeAvg := writeSum / float64(len(writeDurations)) + + sortedWrite := make([]float64, len(writeDurations)) + copy(sortedWrite, writeDurations) + sort.Float64s(sortedWrite) + + wp50 := sortedWrite[int(float64(len(sortedWrite))*0.5)] + wp90 := sortedWrite[int(float64(len(sortedWrite))*0.9)] + wp95 := sortedWrite[int(float64(len(sortedWrite))*0.95)] + wp99 := sortedWrite[int(math.Min(float64(len(sortedWrite))*0.99, float64(len(sortedWrite)-1)))] + + fmt.Println("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Println(" Raw gRPC Benchmark Summary (Go - WRITE)") + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Printf(" Total Batches: %d\n", insertCount) + fmt.Printf(" Batch Size: %d\n", batchCount) + fmt.Printf(" Total Records: %d\n", totalRecords) + fmt.Printf(" Total Inserts: %d (includes %d duplicates)\n", totalInserts, alreadyExistsCount) + fmt.Printf(" Concurrency: %d\n", insertConcurrency) + fmt.Printf(" Total Time: %.2fms\n", float64(writeOverallDuration)) + fmt.Printf(" Inserts/Second: %.2f\n", writeInsertsPerSecond) + fmt.Printf(" Avg Batch Duration: %.2fms\n", writeAvg) + fmt.Printf(" Min Batch Duration: %.2fms\n", writeMin) + fmt.Printf(" Max Batch Duration: %.2fms\n", writeMax) + fmt.Printf(" P50: %.2fms\n", wp50) + fmt.Printf(" P90: %.2fms\n", wp90) + fmt.Printf(" P95: %.2fms\n", wp95) + fmt.Printf(" P99: %.2fms\n", wp99) + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") +} + +func protoValue(val interface{}) *structpb.Value { + if val == nil { + return &structpb.Value{Kind: &structpb.Value_NullValue{NullValue: structpb.NullValue_NULL_VALUE}} + } + switch v := val.(type) { + case string: + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: v}} + case *string: + if v == nil { + return &structpb.Value{Kind: &structpb.Value_NullValue{NullValue: structpb.NullValue_NULL_VALUE}} + } + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: *v}} + case int64: + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: strconv.FormatInt(v, 10)}} + case *int64: + if v == nil { + return &structpb.Value{Kind: &structpb.Value_NullValue{NullValue: structpb.NullValue_NULL_VALUE}} + } + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: strconv.FormatInt(*v, 10)}} + default: + return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("%v", v)}} + } +} + +func sha256Hash(str string) string { + hash := sha256.Sum256([]byte(str)) + return hex.EncodeToString(hash[:]) +} + +func generateUUID() string { + b := make([]byte, 16) + rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +func percentile(sorted []time.Duration, fraction float64) time.Duration { + if len(sorted) == 0 { + return 0 + } + index := int(float64(len(sorted)) * fraction) + if index >= len(sorted) { + index = len(sorted) - 1 + } + return sorted[index] +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} + +func getEnvInt(key string, fallback int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return fallback +} diff --git a/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml index f010c017e92..c8ee6f7595f 100644 --- a/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/go-insert-benchmark.yaml @@ -13,14 +13,14 @@ spec: restartPolicy: Never containers: - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-go:latest + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-go:latest imagePullPolicy: Always resources: requests: - cpu: "3" + cpu: "1" memory: "2Gi" limits: - cpu: "3" + cpu: "1" memory: "2Gi" env: - name: SAMPLE_SIZE @@ -36,8 +36,8 @@ spec: - name: DB_PROJECT_ID value: "span-cloud-testing" - name: DB_INSTANCE - value: "irahul-load-test" + value: "gargsurbhi-testing1" - name: DB_DATABASE - value: "db" + value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml index 4d5e249247b..685d9ff5fed 100644 --- a/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml @@ -13,18 +13,18 @@ spec: restartPolicy: Never containers: - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node-cluster:release-8.6.0 + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node-cluster:release-8.7.1 imagePullPolicy: Always resources: requests: - cpu: "3" + cpu: "1" memory: "3Gi" limits: - cpu: "3" + cpu: "1" memory: "3Gi" env: - name: CLUSTER_WORKERS - value: "3" + value: "1" - name: SAMPLE_SIZE value: "10000" - name: INSERT_COUNT @@ -40,8 +40,8 @@ spec: - name: DB_PROJECT_ID value: "span-cloud-testing" - name: DB_INSTANCE - value: "irahul-load-test" + value: "gargsurbhi-testing1" - name: DB_DATABASE - value: "db" + value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml index 7c1d8b60025..7de90d69726 100644 --- a/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml @@ -13,7 +13,7 @@ spec: restartPolicy: Never containers: - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node-cluster:current + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node-cluster:current imagePullPolicy: Always resources: requests: @@ -40,8 +40,8 @@ spec: - name: DB_PROJECT_ID value: "span-cloud-testing" - name: DB_INSTANCE - value: "irahul-load-test" + value: "gargsurbhi-testing1" - name: DB_DATABASE - value: "db" + value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml index c8bfd27e8f0..d45ad2255bf 100644 --- a/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml @@ -13,7 +13,7 @@ spec: restartPolicy: Never containers: - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node:current + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node:current imagePullPolicy: Always resources: requests: diff --git a/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml index 8772c009abf..c8896ec4f59 100644 --- a/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml @@ -13,14 +13,14 @@ spec: restartPolicy: Never containers: - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/irahul-images/issue-insert-node:release-8.6.0 + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node:release-8.7.1 imagePullPolicy: Always resources: requests: - cpu: "3" + cpu: "1" memory: "2Gi" limits: - cpu: "3" + cpu: "1" memory: "2Gi" env: - name: SAMPLE_SIZE @@ -38,8 +38,10 @@ spec: - name: DB_PROJECT_ID value: "span-cloud-testing" - name: DB_INSTANCE - value: "irahul-load-test" + value: "gargsurbhi-testing1" - name: DB_DATABASE - value: "db" + value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" + - name: SPANNER_DISABLE_BUILT_IN_METRICS + value: "true" \ No newline at end of file diff --git a/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml b/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml new file mode 100644 index 00000000000..448992358c4 --- /dev/null +++ b/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml @@ -0,0 +1,75 @@ +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-raw-grpc-node +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-raw-grpc-node + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-raw-grpc-node:latest + imagePullPolicy: Always + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "1" + memory: "2Gi" + env: + - name: READ_COUNT + value: "10000" + - name: CONCURRENCY + value: "110" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "gargsurbhi-testing1" + - name: DB_DATABASE + value: "jack_henry_db" + - name: TABLE_NAME + value: "tracking.DeviceRecentActivityLog" +--- +apiVersion: batch/v1 +kind: Job +metadata: + namespace: spanner-ns + name: issue-raw-grpc-go +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: issue-raw-grpc-go + spec: + restartPolicy: Never + containers: + - name: benchmark + image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-raw-grpc-go:latest + imagePullPolicy: Always + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "1" + memory: "2Gi" + env: + - name: READ_COUNT + value: "10000" + - name: CONCURRENCY + value: "110" + - name: DB_PROJECT_ID + value: "span-cloud-testing" + - name: DB_INSTANCE + value: "gargsurbhi-testing1" + - name: DB_DATABASE + value: "jack_henry_db" + - name: TABLE_NAME + value: "tracking.DeviceRecentActivityLog" diff --git a/handwritten/spanner/issue/node/package.json b/handwritten/spanner/issue/node/package.json new file mode 100644 index 00000000000..cd4fe44a586 --- /dev/null +++ b/handwritten/spanner/issue/node/package.json @@ -0,0 +1,10 @@ +{ + "name": "raw-grpc-benchmark", + "version": "1.0.0", + "description": "Raw gRPC benchmark for Spanner", + "main": "raw_grpc_benchmark.js", + "dependencies": { + "@google-cloud/spanner": "8.7.1", + "@grpc/grpc-js": "^1.10.0" + } +} \ No newline at end of file diff --git a/handwritten/spanner/issue/node/raw_grpc_benchmark.js b/handwritten/spanner/issue/node/raw_grpc_benchmark.js new file mode 100644 index 00000000000..80bd131a4e7 --- /dev/null +++ b/handwritten/spanner/issue/node/raw_grpc_benchmark.js @@ -0,0 +1,345 @@ +'use strict'; + +const { performance } = require('perf_hooks'); +const crypto = require('crypto'); +const grpc = require('@grpc/grpc-js'); +const grpcGcp = require('grpc-gcp')(grpc); +const gcpApiConfig = require('@google-cloud/spanner/build/src/spanner_grpc_config.json'); +const { v1 } = require('@google-cloud/spanner'); + +// Defaults match what is used in benchmark.js / benchmark-insert-repl.ts +const DB_PROJECT_ID = process.env.DB_PROJECT_ID || 'emulator'; +const DB_INSTANCE = process.env.DB_INSTANCE || 'device-tracking'; +const DB_DATABASE = process.env.DB_DATABASE || 'device-tracking'; +const TABLE_NAME = process.env.TABLE_NAME || 'tracking.DeviceRecentActivityLog'; +const READ_COUNT = envInt('READ_COUNT', 1000); +const CONCURRENCY = envInt('CONCURRENCY', 10); + +// Write benchmark configuration +const SAMPLE_SIZE = envInt('SAMPLE_SIZE', 10000); +const INSERT_COUNT = envInt('INSERT_COUNT', 1000); +const INSERT_CONCURRENCY = envInt('INSERT_CONCURRENCY', 110); +const BATCH_COUNT = envInt('BATCH_COUNT', 1); +const DUPLICATE_INSERT = process.env.DUPLICATE_INSERT === 'true'; + +function sha256(str) { + return crypto.createHash('sha256').update(str).digest('hex'); +} + +function protoValue(val) { + if (val === null || val === undefined) { + return { nullValue: 0 }; + } + return { stringValue: val.toString() }; +} + +async function runBenchmark() { + const databasePath = `projects/${DB_PROJECT_ID}/instances/${DB_INSTANCE}/databases/${DB_DATABASE}`; + + console.log(`Target Database: ${databasePath}`); + console.log(`Target Table: ${TABLE_NAME}`); + console.log(`Read Config: READ_COUNT=${READ_COUNT}, CONCURRENCY=${CONCURRENCY}`); + console.log(`Write Config: SAMPLE_SIZE=${SAMPLE_SIZE}, INSERT_COUNT=${INSERT_COUNT}, INSERT_CONCURRENCY=${INSERT_CONCURRENCY}, BATCH_COUNT=${BATCH_COUNT}, DUPLICATE_INSERT=${DUPLICATE_INSERT}`); + + // Customize grpc-gcp channel pool configuration for a constant 4 channels + const customGcpConfig = JSON.parse(JSON.stringify(gcpApiConfig)); + customGcpConfig.channelPool.minSize = 4; + customGcpConfig.channelPool.maxSize = 4; + + // Initialize a single GAPIC client with grpc-gcp channel pooling enabled + const client = new v1.SpannerClient({ + grpc, + 'grpc.callInvocationTransformer': grpcGcp.gcpCallInvocationTransformer, + 'grpc.channelFactoryOverride': grpcGcp.gcpChannelFactoryOverride, + 'grpc.gcpApiConfig': grpcGcp.createGcpApiConfig(customGcpConfig), + }); + + console.log('\nCreating 4 multiplexed sessions (one per channel) via raw gRPC...'); + const sessions = await Promise.all( + Array.from({ length: 4 }, () => + client.createSession({ + database: databasePath, + session: { + multiplexed: true, + }, + }).then(([s]) => s) + ) + ); + console.log(`Created 4 Multiplexed Sessions.`); + + try { + // ========================================== + // 1. READ BENCHMARK + // ========================================== + const readItems = Array.from({ length: READ_COUNT }); + const readDurations = new Array(READ_COUNT); + + console.log(`\nStarting READ benchmark with concurrency ${CONCURRENCY}...`); + const readOverallStartTime = performance.now(); + + await asyncMap( + readItems, + async (_, i) => { + const session = sessions[i % sessions.length]; + const randomId = Math.floor(Math.random() * 1000000) + 1; + const request = { + session: session.name, + sql: `SELECT * FROM ${TABLE_NAME} WHERE deviceRecentActivityLogId = @id`, + params: { + fields: { + id: { + stringValue: randomId.toString(), + }, + }, + }, + paramTypes: { + id: { + code: 6, // STRING + }, + }, + }; + + const startTime = performance.now(); + await client.executeSql(request); + const duration = performance.now() - startTime; + + readDurations[i] = duration; + }, + { concurrency: CONCURRENCY } + ); + + const readOverallDuration = performance.now() - readOverallStartTime; + const readOpsPerSecond = READ_COUNT / (readOverallDuration / 1000); + + const readSum = readDurations.reduce((a, b) => a + b, 0); + const readAvg = readSum / readDurations.length; + + const readSorted = [...readDurations].sort((a, b) => a - b); + const rp50 = percentile(readSorted, 0.5); + const rp90 = percentile(readSorted, 0.9); + const rp95 = percentile(readSorted, 0.95); + const rp99 = percentile(readSorted, 0.99); + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(' Raw gRPC Benchmark Summary (Node.js - READ)'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(` Total Reads: ${READ_COUNT}`); + console.log(` Concurrency: ${CONCURRENCY}`); + console.log(` Total Time: ${readOverallDuration.toFixed(2)}ms`); + console.log(` Reads/Second: ${readOpsPerSecond.toFixed(2)}`); + console.log(` Avg Latency: ${readAvg.toFixed(2)}ms`); + console.log(` P50 Latency: ${rp50.toFixed(2)}ms`); + console.log(` P90 Latency: ${rp90.toFixed(2)}ms`); + console.log(` P95 Latency: ${rp95.toFixed(2)}ms`); + console.log(` P99 Latency: ${rp99.toFixed(2)}ms`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // ========================================== + // 2. SLEEP 5 SECONDS + // ========================================== + console.log('Sleeping for 5 seconds before starting writes...'); + await new Promise(resolve => setTimeout(resolve, 5000)); + + // ========================================== + // 3. WRITE BENCHMARK + // ========================================== + console.log(`\nStarting WRITE benchmark with concurrency ${INSERT_CONCURRENCY}...`); + + const totalRecords = INSERT_COUNT * BATCH_COUNT; + const totalInserts = DUPLICATE_INSERT ? totalRecords * 2 : totalRecords; + + // Pre-generate batches + const batches = []; + for (let i = 0; i < INSERT_COUNT; i++) { + const batch = []; + for (let j = 0; j < BATCH_COUNT; j++) { + const idx = i * BATCH_COUNT + j; + const deviceRecordId = sha256(`device-${idx % SAMPLE_SIZE}`); + const deviceDetailsId = sha256(`detail-${idx % SAMPLE_SIZE}`); + const httpRequestDetailsId = sha256(`request-${idx % SAMPLE_SIZE}`); + const httpRequestLocationId = sha256(`location-${idx % SAMPLE_SIZE}`); + const xRequestId = crypto.randomUUID(); + const sessionId = crypto.randomUUID(); + const createdAt = new Date(Date.now() + idx * 1000); + + const deviceRecentActivityLogId = sha256(`benchmark-${idx}${deviceDetailsId}${httpRequestDetailsId}${createdAt.getTime()}${sessionId}`); + + batch.push({ + deviceRecentActivityLogId, + deviceRecordId, + deviceDetailsId, + httpRequestDetailsId, + ipAddress: Buffer.from([192, 168, 1, 100]).toString('base64'), + institutionId: 'benchmark-institution', + userId: Math.random() > 0.5 ? 'user-123' : null, + username: null, + xRequestId, + httpRequestLocationId, + latency: Math.random() > 0.5 ? Math.floor(Math.random() * 100) : null, + sessionId, + createdAt: createdAt.toISOString(), + }); + } + batches.push(batch); + } + + const writeDurations = []; + let alreadyExistsCount = 0; + const writeOverallStartTime = performance.now(); + + await asyncMap( + batches, + async (batch, i) => { + const startTime = performance.now(); + + const mutations = batch.map(rec => ({ + insert: { + table: TABLE_NAME, + columns: [ + 'deviceRecentActivityLogId', + 'deviceRecordId', + 'deviceDetailsId', + 'httpRequestDetailsId', + 'ipAddress', + 'institutionId', + 'userId', + 'username', + 'xRequestId', + 'httpRequestLocationId', + 'latency', + 'sessionId', + 'createdAt', + ], + values: [ + { + values: [ + protoValue(rec.deviceRecentActivityLogId), + protoValue(rec.deviceRecordId), + protoValue(rec.deviceDetailsId), + protoValue(rec.httpRequestDetailsId), + protoValue(rec.ipAddress), + protoValue(rec.institutionId), + protoValue(rec.userId), + protoValue(rec.username), + protoValue(rec.xRequestId), + protoValue(rec.httpRequestLocationId), + protoValue(rec.latency), + protoValue(rec.sessionId), + protoValue(rec.createdAt), + ], + }, + ], + }, + })); + + const session = sessions[i % sessions.length]; + + const commitRequest = { + session: session.name, + singleUseTransaction: { + readWrite: {}, + }, + mutations, + }; + + try { + await client.commit(commitRequest); + } catch (e) { + if (e.code === 6) { // ALREADY_EXISTS + alreadyExistsCount++; + } else { + throw e; + } + } + + if (DUPLICATE_INSERT) { + try { + await client.commit(commitRequest); + } catch (e) { + if (e.code === 6) { + alreadyExistsCount++; + } else { + throw e; + } + } + } + + const duration = performance.now() - startTime; + writeDurations.push(duration); + + if ((i + 1) % 100 === 0 || (i + 1) === INSERT_COUNT) { + const slice = writeDurations.slice(-100); + const avg = slice.reduce((a, b) => a + b, 0) / slice.length; + console.log(` [${i + 1}/${INSERT_COUNT}] Last batches avg: ${avg.toFixed(2)}ms (${BATCH_COUNT} records/batch)`); + } + }, + { concurrency: INSERT_CONCURRENCY } + ); + + const writeOverallDuration = performance.now() - writeOverallStartTime; + const writeInsertsPerSecond = totalInserts / (writeOverallDuration / 1000); + + const writeSum = writeDurations.reduce((a, b) => a + b, 0); + const writeAvg = writeSum / writeDurations.length; + const writeMin = Math.min(...writeDurations); + const writeMax = Math.max(...writeDurations); + + const writeSorted = [...writeDurations].sort((a, b) => a - b); + const wp50 = percentile(writeSorted, 0.5); + const wp90 = percentile(writeSorted, 0.9); + const wp95 = percentile(writeSorted, 0.95); + const wp99 = percentile(writeSorted, 0.99); + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(' Raw gRPC Benchmark Summary (Node.js - WRITE)'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(` Total Batches: ${INSERT_COUNT}`); + console.log(` Batch Size: ${BATCH_COUNT}`); + console.log(` Total Records: ${totalRecords}`); + console.log(` Total Inserts: ${totalInserts} (includes ${alreadyExistsCount} duplicates)`); + console.log(` Concurrency: ${INSERT_CONCURRENCY}`); + console.log(` Total Time: ${writeOverallDuration.toFixed(2)}ms`); + console.log(` Inserts/Second: ${writeInsertsPerSecond.toFixed(2)}`); + console.log(` Avg Batch Duration: ${writeAvg.toFixed(2)}ms`); + console.log(` Min Batch Duration: ${writeMin.toFixed(2)}ms`); + console.log(` Max Batch Duration: ${writeMax.toFixed(2)}ms`); + console.log(` P50: ${wp50.toFixed(2)}ms`); + console.log(` P90: ${wp90.toFixed(2)}ms`); + console.log(` P95: ${wp95.toFixed(2)}ms`); + console.log(` P99: ${wp99.toFixed(2)}ms`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + } finally { + console.log('\nBenchmark finished. Multiplexed session is left to be managed by server.'); + } +} + +async function asyncMap(items, mapperFn, opts = {}) { + const concurrency = Math.max(1, opts.concurrency ?? Infinity); + const results = new Array(items.length); + let currentIndex = 0; + + async function processNext() { + while (currentIndex < items.length) { + const index = currentIndex++; + results[index] = await mapperFn(items[index], index); + } + } + + const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => processNext()); + await Promise.all(workers); + return results; +} + +function percentile(sorted, fraction) { + if (!sorted.length) return 0; + const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction)); + return sorted[index]; +} + +function envInt(key, fallback) { + const value = process.env[key]; + return value === undefined || value === '' ? fallback : parseInt(value, 10); +} + +runBenchmark().catch(console.error); diff --git a/handwritten/spanner/issue/seed-reference-data.js b/handwritten/spanner/issue/node/seed-reference-data.js similarity index 86% rename from handwritten/spanner/issue/seed-reference-data.js rename to handwritten/spanner/issue/node/seed-reference-data.js index 65cc842ccf6..d9d393fb5ab 100644 --- a/handwritten/spanner/issue/seed-reference-data.js +++ b/handwritten/spanner/issue/node/seed-reference-data.js @@ -1,17 +1,17 @@ 'use strict'; const crypto = require('crypto'); -const {Spanner} = require('@google-cloud/spanner'); +const { Spanner } = require('@google-cloud/spanner'); const SAMPLE_SIZE = envInt('SAMPLE_SIZE', 10000); const DB_PROJECT_ID = process.env.DB_PROJECT_ID || 'span-cloud-testing'; -const DB_INSTANCE = process.env.DB_INSTANCE || 'irahul-load-test'; -const DB_DATABASE = process.env.DB_DATABASE || 'db'; +const DB_INSTANCE = process.env.DB_INSTANCE || 'gargsurbhi-testing1'; +const DB_DATABASE = process.env.DB_DATABASE || 'jack_henry_db'; const DB_SCHEMA = process.env.DB_SCHEMA || 'tracking'; const BATCH_SIZE = envInt('SEED_BATCH_SIZE', 500); async function main() { - const spanner = new Spanner({projectId: DB_PROJECT_ID, disableBuiltInMetrics: true}); + const spanner = new Spanner({ projectId: DB_PROJECT_ID, disableBuiltInMetrics: true }); const database = spanner.instance(DB_INSTANCE).database(DB_DATABASE); try { await seedTable(database, `${DB_SCHEMA}.Devices`, 'deviceRecordId', 'device'); diff --git a/handwritten/spanner/src/instrument.ts b/handwritten/spanner/src/instrument.ts index 9537e51efd9..ec0f8c460ad 100644 --- a/handwritten/spanner/src/instrument.ts +++ b/handwritten/spanner/src/instrument.ts @@ -104,6 +104,10 @@ const { * spans resulting from async/await invocations won't be correctly * associated in their respective hierarchies. */ +/** + * Safely ensures that the OpenTelemetry context manager is initialized + * only if no specialized manager is currently active globally. + */ function ensureInitialContextManagerSet() { if (!context['_contextManager'] || context.active() === ROOT_CONTEXT) { // If no context manager is currently set, or if the active context is the ROOT_CONTEXT, From 65357ea243a45a2e1aaf179436fe014002b051b4 Mon Sep 17 00:00:00 2001 From: Surbhi Garg Date: Fri, 15 May 2026 13:14:28 +0530 Subject: [PATCH 7/7] code optimization and removing cluster scripts --- handwritten/spanner/.gitignore | 2 + handwritten/spanner/issue/Dockerfile.node | 2 +- .../spanner/issue/Dockerfile.node-cluster | 11 ---- .../issue/Dockerfile.node-cluster-current | 28 --------- .../spanner/issue/Dockerfile.node-current | 2 +- handwritten/spanner/issue/build-images.sh | 37 +++--------- .../k8s/node-cluster-insert-benchmark.yaml | 47 --------------- ...node-current-cluster-insert-benchmark.yaml | 47 --------------- .../k8s/node-current-insert-benchmark.yaml | 19 +++--- .../issue/k8s/node-insert-benchmark.yaml | 9 ++- .../spanner/issue/k8s/raw-grpc-benchmark.yaml | 7 +++ .../issue/{ => node}/benchmark-insert-repl.ts | 29 +++++---- .../spanner/issue/node/cluster-runner.js | 59 ------------------ handwritten/spanner/issue/node/package.json | 9 ++- handwritten/spanner/issue/node/tsconfig.json | 24 ++++++++ .../spanner/src/common-grpc/service.ts | 2 +- handwritten/spanner/src/index.ts | 21 ++++--- handwritten/spanner/src/instrument.ts | 60 +------------------ handwritten/spanner/src/request_id_header.ts | 22 +------ 19 files changed, 103 insertions(+), 334 deletions(-) delete mode 100644 handwritten/spanner/issue/Dockerfile.node-cluster delete mode 100644 handwritten/spanner/issue/Dockerfile.node-cluster-current delete mode 100644 handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml delete mode 100644 handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml rename handwritten/spanner/issue/{ => node}/benchmark-insert-repl.ts (95%) delete mode 100644 handwritten/spanner/issue/node/cluster-runner.js create mode 100644 handwritten/spanner/issue/node/tsconfig.json diff --git a/handwritten/spanner/.gitignore b/handwritten/spanner/.gitignore index d4f03a0df2e..234db77e580 100644 --- a/handwritten/spanner/.gitignore +++ b/handwritten/spanner/.gitignore @@ -4,6 +4,7 @@ /coverage /.nyc_output /docs/ +dist/ /out/ /build/ system-test/secrets.js @@ -11,4 +12,5 @@ system-test/*key.json *.lock .DS_Store package-lock.json +*.cpuprofile __pycache__ diff --git a/handwritten/spanner/issue/Dockerfile.node b/handwritten/spanner/issue/Dockerfile.node index 02acf19fa0a..a090346de84 100644 --- a/handwritten/spanner/issue/Dockerfile.node +++ b/handwritten/spanner/issue/Dockerfile.node @@ -5,6 +5,6 @@ ARG SPANNER_VERSION=8.7.1 RUN npm init -y \ && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js tsx \ && npm cache clean --force -COPY --chown=node:node --chmod=0444 issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +COPY --chown=node:node --chmod=0444 issue/node/benchmark-insert-repl.ts ./benchmark-insert-repl.mts USER node ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster b/handwritten/spanner/issue/Dockerfile.node-cluster deleted file mode 100644 index f9fb8614555..00000000000 --- a/handwritten/spanner/issue/Dockerfile.node-cluster +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:22-bookworm-slim -ENV NODE_ENV=production -WORKDIR /app -ARG SPANNER_VERSION=8.7.1 -RUN npm init -y \ - && npm install --omit=dev "@google-cloud/spanner@${SPANNER_VERSION}" @grpc/grpc-js \ - && npm cache clean --force -COPY --chown=node:node --chmod=0444 issue/node/benchmark.js ./benchmark.js -COPY --chown=node:node --chmod=0444 issue/node/cluster-runner.js ./cluster-runner.js -USER node -ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-cluster-current b/handwritten/spanner/issue/Dockerfile.node-cluster-current deleted file mode 100644 index b8289d99cb2..00000000000 --- a/handwritten/spanner/issue/Dockerfile.node-cluster-current +++ /dev/null @@ -1,28 +0,0 @@ -FROM node:22-bookworm-slim AS local-lib -WORKDIR /workspace/spanner -COPY package*.json ./ -RUN if [ -f package-lock.json ]; then \ - npm ci --ignore-scripts; \ - else \ - npm install --ignore-scripts; \ - fi -COPY . . -RUN npm run compile \ - && mkdir -p /tmp/spanner-pack \ - && npm pack --pack-destination /tmp/spanner-pack \ - && cp /tmp/spanner-pack/google-cloud-spanner-*.tgz /tmp/google-cloud-spanner.tgz - -FROM node:22-bookworm-slim -ENV NODE_ENV=production -WORKDIR /app -RUN npm init -y \ - && npm install --omit=dev @grpc/grpc-js \ - && npm cache clean --force -COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tgz -RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ - && npm cache clean --force \ - && node -e "console.log(require('@google-cloud/spanner/package.json').version)" -COPY --chown=node:node --chmod=0444 issue/node/benchmark.js ./benchmark.js -COPY --chown=node:node --chmod=0444 issue/node/cluster-runner.js ./cluster-runner.js -USER node -ENTRYPOINT ["node", "/app/cluster-runner.js"] diff --git a/handwritten/spanner/issue/Dockerfile.node-current b/handwritten/spanner/issue/Dockerfile.node-current index 4097c096f37..0135e6e77c2 100644 --- a/handwritten/spanner/issue/Dockerfile.node-current +++ b/handwritten/spanner/issue/Dockerfile.node-current @@ -22,6 +22,6 @@ COPY --from=local-lib /tmp/google-cloud-spanner.tgz /tmp/google-cloud-spanner.tg RUN npm install --omit=dev /tmp/google-cloud-spanner.tgz \ && npm cache clean --force \ && node -e "console.log(require('@google-cloud/spanner/package.json').version)" -COPY --chown=node:node --chmod=0444 issue/benchmark-insert-repl.ts ./benchmark-insert-repl.mts +COPY --chown=node:node --chmod=0444 issue/node/benchmark-insert-repl.ts ./benchmark-insert-repl.mts USER node ENTRYPOINT ["/app/node_modules/.bin/tsx", "/app/benchmark-insert-repl.mts"] diff --git a/handwritten/spanner/issue/build-images.sh b/handwritten/spanner/issue/build-images.sh index 93ca699c267..70f87fba854 100755 --- a/handwritten/spanner/issue/build-images.sh +++ b/handwritten/spanner/issue/build-images.sh @@ -3,10 +3,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" IMAGE_REPO="${IMAGE_REPO:-us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images}" -SPANNER_VERSION="${SPANNER_VERSION:-8.7.1}" +SPANNER_VERSION="${SPANNER_VERSION:-8.17.1}" PUSH="${PUSH:-true}" PLATFORM="${PLATFORM:-linux/amd64}" -BUILD_CURRENT="${BUILD_CURRENT:-false}" CONTAINER_TOOL="${CONTAINER_TOOL:-podman}" # Build Node Raw gRPC image @@ -14,49 +13,29 @@ $CONTAINER_TOOL build --platform linux/amd64 -f issue/Dockerfile.node-raw -t $IM # Build Go Raw gRPC image $CONTAINER_TOOL build --platform linux/amd64 -f issue/Dockerfile.go-raw -t $IMAGE_REPO/issue-raw-grpc-go:latest . - - +# Build Go image "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.go" \ -t "$IMAGE_REPO/issue-insert-go:latest" \ "$ROOT" +# Build Node image "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node" \ --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ -t "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" \ -t "$IMAGE_REPO/issue-insert-node:latest" \ "$ROOT" -# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster" \ -# --build-arg "SPANNER_VERSION=$SPANNER_VERSION" \ -# -t "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ -# "$ROOT" +# Build Node image with current code changes +"$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ + -t "$IMAGE_REPO/issue-insert-node:current" \ + "$ROOT" -# if [[ "$BUILD_CURRENT" == "true" ]]; then -# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-current" \ -# -t "$IMAGE_REPO/issue-insert-node:current" \ -# "$ROOT" -# "$CONTAINER_TOOL" build --platform "$PLATFORM" -f "$ROOT/issue/Dockerfile.node-cluster-current" \ -# -t "$IMAGE_REPO/issue-insert-node-cluster:current" \ -# -t "$IMAGE_REPO/issue-insert-node-cluster:latest" \ -# "$ROOT" -# else -# "$CONTAINER_TOOL" tag "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" \ -# "$IMAGE_REPO/issue-insert-node-cluster:latest" -# fi if [[ "$PUSH" == "true" ]]; then -# Push Raw gRPC images $CONTAINER_TOOL push $IMAGE_REPO/issue-raw-grpc-node:latest $CONTAINER_TOOL push $IMAGE_REPO/issue-raw-grpc-go:latest "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-go:latest" "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:release-${SPANNER_VERSION}" "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:latest" - # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:release-${SPANNER_VERSION}" - # if [[ "$BUILD_CURRENT" == "true" ]]; then - # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:current" - # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:current" - # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:latest" - # else - # "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node-cluster:latest" - # fi + "$CONTAINER_TOOL" push "$IMAGE_REPO/issue-insert-node:current" fi diff --git a/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml deleted file mode 100644 index 685d9ff5fed..00000000000 --- a/handwritten/spanner/issue/k8s/node-cluster-insert-benchmark.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - namespace: spanner-ns - name: issue-insert-node-cluster -spec: - backoffLimit: 0 - template: - metadata: - labels: - app: issue-insert-node-cluster - spec: - restartPolicy: Never - containers: - - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node-cluster:release-8.7.1 - imagePullPolicy: Always - resources: - requests: - cpu: "1" - memory: "3Gi" - limits: - cpu: "1" - memory: "3Gi" - env: - - name: CLUSTER_WORKERS - value: "1" - - name: SAMPLE_SIZE - value: "10000" - - name: INSERT_COUNT - value: "1000" - - name: INSERT_CONCURRENCY - value: "24" - - name: BATCH_COUNT - value: "1" - - name: DUPLICATE_INSERT - value: "false" - - name: VERBOSE_BATCH_LOGS - value: "true" - - name: DB_PROJECT_ID - value: "span-cloud-testing" - - name: DB_INSTANCE - value: "gargsurbhi-testing1" - - name: DB_DATABASE - value: "jack_henry_db" - - name: DB_SCHEMA - value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml deleted file mode 100644 index 7de90d69726..00000000000 --- a/handwritten/spanner/issue/k8s/node-current-cluster-insert-benchmark.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - namespace: spanner-ns - name: issue-insert-node-current-cluster -spec: - backoffLimit: 0 - template: - metadata: - labels: - app: issue-insert-node-current-cluster - spec: - restartPolicy: Never - containers: - - name: benchmark - image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node-cluster:current - imagePullPolicy: Always - resources: - requests: - cpu: "3" - memory: "3Gi" - limits: - cpu: "3" - memory: "3Gi" - env: - - name: CLUSTER_WORKERS - value: "3" - - name: SAMPLE_SIZE - value: "10000" - - name: INSERT_COUNT - value: "1000" - - name: INSERT_CONCURRENCY - value: "24" - - name: BATCH_COUNT - value: "1" - - name: DUPLICATE_INSERT - value: "false" - - name: VERBOSE_BATCH_LOGS - value: "true" - - name: DB_PROJECT_ID - value: "span-cloud-testing" - - name: DB_INSTANCE - value: "gargsurbhi-testing1" - - name: DB_DATABASE - value: "jack_henry_db" - - name: DB_SCHEMA - value: "tracking" diff --git a/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml index d45ad2255bf..b9022dfeb79 100644 --- a/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-current-insert-benchmark.yaml @@ -15,6 +15,13 @@ spec: - name: benchmark image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node:current imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting V8 CPU Profiler benchmark with tsx..." + /app/node_modules/.bin/tsx --cpu-prof --cpu-prof-dir=/tmp --cpu-prof-name=k8s-insert-current.cpuprofile /app/benchmark-insert-repl.mts + echo "✓ Benchmark finished! Sleeping for 1 hour so you can kubectl cp the profile..." + sleep 3600 resources: requests: cpu: "3" @@ -35,15 +42,13 @@ spec: value: "false" - name: VERBOSE_BATCH_LOGS value: "true" - - name: SPANNER_DISABLE_BUILT_IN_METRICS - value: "true" - - name: SPANNER_DISABLE_BUILTIN_METRICS - value: "true" - name: DB_PROJECT_ID - value: "emulator" + value: "span-cloud-testing" - name: DB_INSTANCE - value: "device-tracking" + value: "gargsurbhi-testing1" - name: DB_DATABASE - value: "device-tracking" + value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" + - name: SPANNER_DISABLE_BUILTIN_METRICS + value: "true" diff --git a/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml index c8896ec4f59..5a148a8847d 100644 --- a/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/node-insert-benchmark.yaml @@ -15,6 +15,13 @@ spec: - name: benchmark image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-insert-node:release-8.7.1 imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting V8 CPU Profiler benchmark with tsx..." + /app/node_modules/.bin/tsx --cpu-prof --cpu-prof-dir=/tmp --cpu-prof-name=k8s-insert.cpuprofile /app/benchmark-insert-repl.mts + echo "✓ Benchmark finished! Sleeping for 1 hour so you can kubectl cp the profile..." + sleep 3600 resources: requests: cpu: "1" @@ -43,5 +50,5 @@ spec: value: "jack_henry_db" - name: DB_SCHEMA value: "tracking" - - name: SPANNER_DISABLE_BUILT_IN_METRICS + - name: SPANNER_DISABLE_BUILTIN_METRICS value: "true" \ No newline at end of file diff --git a/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml b/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml index 448992358c4..fded591b703 100644 --- a/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml +++ b/handwritten/spanner/issue/k8s/raw-grpc-benchmark.yaml @@ -15,6 +15,13 @@ spec: - name: benchmark image: us-central1-docker.pkg.dev/span-cloud-testing/gargsurbhi-images/issue-raw-grpc-node:latest imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting Raw gRPC CPU Profiler benchmark..." + node --cpu-prof --cpu-prof-dir=/tmp --cpu-prof-name=k8s-raw-grpc.cpuprofile /app/raw_grpc_benchmark.js + echo "✓ Benchmark finished! Sleeping for 1 hour so you can kubectl cp the profile..." + sleep 3600 resources: requests: cpu: "1" diff --git a/handwritten/spanner/issue/benchmark-insert-repl.ts b/handwritten/spanner/issue/node/benchmark-insert-repl.ts similarity index 95% rename from handwritten/spanner/issue/benchmark-insert-repl.ts rename to handwritten/spanner/issue/node/benchmark-insert-repl.ts index fd2b8425be9..5c7bc165f1a 100644 --- a/handwritten/spanner/issue/benchmark-insert-repl.ts +++ b/handwritten/spanner/issue/node/benchmark-insert-repl.ts @@ -34,15 +34,15 @@ const DB_SCHEMA = process.env.DB_SCHEMA || 'tracking' const POOL_OPTIONS = process.env.POOL_OPTIONS ? JSON.parse(process.env.POOL_OPTIONS) : { - max: 20, - min: 1, - incStep: 5, - maxIdle: 1, - idlesAfter: 1, - keepAlive: 10, - acquireTimeout: 10_000, - fail: false, - } + max: 400, + // min: 1, + // incStep: 5, + // maxIdle: 1, + // idlesAfter: 1, + // keepAlive: 10, + // acquireTimeout: 10_000, + // fail: false, + } async function runBenchmark() { // Dynamic imports for REPL compatibility @@ -285,9 +285,13 @@ async function runBenchmark() { 2, ) + const spannerVersion = (spanner as any).options?.libVersion || 'NA' + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') console.log(' Benchmark Summary') console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log(` Spanner SDK Version: ${spannerVersion}`) + console.log(` SPANNER_DISABLE_BUILTIN_METRICS: ${process.env.SPANNER_DISABLE_BUILTIN_METRICS}`) console.log(` Total Batches: ${INSERT_COUNT}`) console.log(` Batch Size: ${BATCH_COUNT}`) console.log(` Total Records: ${totalRecords}`) @@ -320,7 +324,10 @@ async function runBenchmark() { } // Export for REPL usage -;(globalThis as any).runBenchmark = runBenchmark +; (globalThis as any).runBenchmark = runBenchmark console.log('✓ Benchmark loaded. Run with: await runBenchmark()') -await runBenchmark() +runBenchmark().catch((err) => { + console.error("Unhandled fatal exception in main context:", err); + process.exit(1); +}); diff --git a/handwritten/spanner/issue/node/cluster-runner.js b/handwritten/spanner/issue/node/cluster-runner.js deleted file mode 100644 index ac5ad046b34..00000000000 --- a/handwritten/spanner/issue/node/cluster-runner.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const cluster = require('cluster'); -const os = require('os'); -const {performance} = require('perf_hooks'); -const {aggregateSummaries, printSummary, runBenchmark} = require('./benchmark'); - -const workerCount = parseInt(process.env.CLUSTER_WORKERS || process.env.PM2_INSTANCES || '3', 10) || os.availableParallelism?.() || os.cpus().length; - -if (cluster.isPrimary) { - const start = performance.now(); - const summaries = []; - let failed = false; - - console.log(`Starting node cluster benchmark with ${workerCount} workers`); - for (let i = 0; i < workerCount; i++) { - const worker = cluster.fork({ - ...process.env, - WORKER_INDEX: String(i), - WORKER_COUNT: String(workerCount), - CLUSTER_CHILD: 'true', - }); - worker.on('message', message => { - if (message?.type === 'summary') summaries.push(message.summary); - if (message?.type === 'error') { - failed = true; - console.error(`worker ${i} failed: ${message.error}`); - } - }); - } - - cluster.on('exit', (worker, code, signal) => { - if (code !== 0) { - failed = true; - console.error(`worker ${worker.id} exited with code=${code} signal=${signal}`); - } - if (Object.keys(cluster.workers).length === 0) { - const totalWallTimeMs = performance.now() - start; - if (summaries.length) { - const aggregate = aggregateSummaries(summaries, totalWallTimeMs); - printSummary(aggregate, 'Combined Node Cluster Benchmark Summary'); - } - process.exit(failed ? 1 : 0); - } - }); -} else { - runBenchmark({ - workerIndex: parseInt(process.env.WORKER_INDEX || '0', 10), - workerCount: parseInt(process.env.WORKER_COUNT || '1', 10), - }) - .then(summary => { - if (process.send) process.send({type: 'summary', summary}); - }) - .catch(err => { - if (process.send) process.send({type: 'error', error: err.stack || err.message}); - console.error(err); - process.exit(1); - }); -} diff --git a/handwritten/spanner/issue/node/package.json b/handwritten/spanner/issue/node/package.json index cd4fe44a586..f8d3f7f2dd7 100644 --- a/handwritten/spanner/issue/node/package.json +++ b/handwritten/spanner/issue/node/package.json @@ -3,8 +3,15 @@ "version": "1.0.0", "description": "Raw gRPC benchmark for Spanner", "main": "raw_grpc_benchmark.js", + "scripts": { + "build": "tsc" + }, "dependencies": { - "@google-cloud/spanner": "8.7.1", + "@google-cloud/spanner": "file:../..", "@grpc/grpc-js": "^1.10.0" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.3.3" } } \ No newline at end of file diff --git a/handwritten/spanner/issue/node/tsconfig.json b/handwritten/spanner/issue/node/tsconfig.json new file mode 100644 index 00000000000..82732982d79 --- /dev/null +++ b/handwritten/spanner/issue/node/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "es2022" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./", + "declaration": true + }, + "include": [ + "benchmark-insert-repl.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/handwritten/spanner/src/common-grpc/service.ts b/handwritten/spanner/src/common-grpc/service.ts index ba7691ce5b1..71d0b5ca794 100644 --- a/handwritten/spanner/src/common-grpc/service.ts +++ b/handwritten/spanner/src/common-grpc/service.ts @@ -992,7 +992,7 @@ export class GrpcService extends Service { delete reqOpts.autoPaginateVal; delete reqOpts.objectMode; - return replaceProjectIdToken(reqOpts, this.projectId); + return reqOpts; } /** diff --git a/handwritten/spanner/src/index.ts b/handwritten/spanner/src/index.ts index 50d393249fe..4ae082c0517 100644 --- a/handwritten/spanner/src/index.ts +++ b/handwritten/spanner/src/index.ts @@ -501,7 +501,7 @@ class Spanner extends GrpcService { this.projectFormattedName_, this._observabilityOptions?.enableEndToEndTracing, ); - ensureInitialContextManagerSet(); + // ensureInitialContextManagerSet(); this._nthClientId = nextSpannerClientId(); this._universeDomain = universeEndpoint; this.projectId_ = options.projectId; @@ -1691,13 +1691,10 @@ class Spanner extends GrpcService { callback(err, null); } const gaxClient = this.clients_.get(clientName)!; - let reqOpts = extend(true, {}, config.reqOpts); - reqOpts = replaceProjectIdToken(reqOpts, projectId!); - // It would have been preferable to replace the projectId already in the - // constructor of Spanner, but that is not possible as auth.getProjectId - // is an async method. This is therefore the first place where we have - // access to the value that should be used instead of the placeholder. + let reqOpts; if (!this.projectIdReplaced_) { + reqOpts = extend(true, {}, config.reqOpts); + reqOpts = replaceProjectIdToken(reqOpts, projectId!); this.projectId = replaceProjectIdToken(this.projectId, projectId!); this.projectFormattedName_ = replaceProjectIdToken( this.projectFormattedName_, @@ -1715,12 +1712,14 @@ class Spanner extends GrpcService { ); }); }); + config.headers[CLOUD_RESOURCE_HEADER] = replaceProjectIdToken( + config.headers[CLOUD_RESOURCE_HEADER], + projectId!, + ); this.projectIdReplaced_ = true; + } else { + reqOpts = config.reqOpts; } - config.headers[CLOUD_RESOURCE_HEADER] = replaceProjectIdToken( - config.headers[CLOUD_RESOURCE_HEADER], - projectId!, - ); // Do context propagation propagation.inject(context.active(), config.headers, { set: (carrier, key, value) => { diff --git a/handwritten/spanner/src/instrument.ts b/handwritten/spanner/src/instrument.ts index ec0f8c460ad..a4b46b38bfe 100644 --- a/handwritten/spanner/src/instrument.ts +++ b/handwritten/spanner/src/instrument.ts @@ -136,61 +136,7 @@ export function startTrace( config: traceConfig | undefined, cb: (span: Span) => T, ): T { - if (!config) { - config = {} as traceConfig; - } - - return getTracer(config.opts?.tracerProvider).startActiveSpan( - SPAN_NAMESPACE_PREFIX + '.' + spanNameSuffix, - {kind: SpanKind.CLIENT}, - span => { - span.setAttribute(ATTR_OTEL_SCOPE_NAME, TRACER_NAME); - span.setAttribute(ATTR_OTEL_SCOPE_VERSION, TRACER_VERSION); - span.setAttribute('gcp.client.service', 'spanner'); - span.setAttribute('gcp.client.version', TRACER_VERSION); - span.setAttribute('gcp.client.repo', 'googleapis/nodejs-spanner'); - - if (config.tableName) { - span.setAttribute('db.sql.table', config.tableName); - } - if (config.dbName) { - span.setAttribute( - 'gcp.resource.name', - `//spanner.googleapis.com/${config.dbName}`, - ); - span.setAttribute('db.name', config.dbName); - } - if (config.requestTag) { - span.setAttribute('request.tag', config.requestTag); - } - if (config.transactionTag) { - span.setAttribute('transaction.tag', config.transactionTag); - } - - const allowExtendedTracing = - optedInPII || config.opts?.enableExtendedTracing; - if (config.sql && allowExtendedTracing) { - const sql = config.sql; - if (typeof sql === 'string') { - span.setAttribute('db.statement', sql as string); - } else { - const stmt = sql as SQLStatement; - span.setAttribute('db.statement', stmt.sql); - } - } - - // If at all the invoked function throws an exception, - // record the exception and then end this span. - try { - return cb(span); - } catch (e) { - setSpanErrorAndException(span, e as Error); - span.end(); - // Finally re-throw the exception. - throw e; - } - }, - ); + return cb(new noopSpan()); } /** @@ -249,10 +195,6 @@ export function setSpanErrorAndException( * @returns {Span} the non-null span. */ export function getActiveOrNoopSpan(): Span { - const span = trace.getActiveSpan(); - if (span) { - return span; - } return new noopSpan(); } diff --git a/handwritten/spanner/src/request_id_header.ts b/handwritten/spanner/src/request_id_header.ts index 99c081de64c..0f079c61e0d 100644 --- a/handwritten/spanner/src/request_id_header.ts +++ b/handwritten/spanner/src/request_id_header.ts @@ -133,20 +133,7 @@ function injectRequestIDIntoHeaders( nthRequest?: number, attempt?: number, ) { - if (!session) { - return headers; - } - - if (!nthRequest) { - const database = session.parent as withNextNthRequest; - if (!(database && typeof database._nextNthRequest === 'function')) { - return headers; - } - nthRequest = database._nextNthRequest(); - } - - attempt = attempt || 1; - return _metadataWithRequestId(session, nthRequest!, attempt, headers); + return headers; } function _metadataWithRequestId( @@ -197,12 +184,7 @@ const X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR = 'x_goog_spanner_request_id'; * long after tracing has been performed. */ function attributeXGoogSpannerRequestIdToActiveSpan(config: any) { - const reqId = extractRequestID(config); - if (!(reqId && reqId.length > 0)) { - return; - } - const span = getActiveOrNoopSpan(); - span.setAttribute(X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR, reqId); + return; } const X_GOOG_REQ_ID_REGEX = /^1\.[0-9A-Fa-f]{8}(\.\d+){3}\.\d+/;