scripts to run benchmarks
This commit is contained in:
186
benchmarks/e2e/bench_test.go
Normal file
186
benchmarks/e2e/bench_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package e2e_bench_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── BenchmarkBAPCaller_Discover ───────────────────────────────────────────────
|
||||
// Baseline single-goroutine throughput and latency for the discover endpoint.
|
||||
// Exercises the full bapTxnCaller pipeline: addRoute → sign → validateSchema.
|
||||
func BenchmarkBAPCaller_Discover(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := buildSignedRequest(b, "discover")
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BenchmarkBAPCaller_Discover_Parallel ─────────────────────────────────────
|
||||
// Measures throughput under concurrent load. Run with -cpu=1,2,4,8,16 to
|
||||
// produce a concurrency sweep. Each goroutine runs its own request loop.
|
||||
func BenchmarkBAPCaller_Discover_Parallel(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
req := buildSignedRequest(b, "discover")
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("parallel: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── BenchmarkBAPCaller_AllActions ────────────────────────────────────────────
|
||||
// Measures per-action latency for discover, select, init, and confirm in a
|
||||
// single benchmark run. Each sub-benchmark is independent.
|
||||
func BenchmarkBAPCaller_AllActions(b *testing.B) {
|
||||
actions := []string{"discover", "select", "init", "confirm"}
|
||||
|
||||
for _, action := range actions {
|
||||
action := action // capture for sub-benchmark closure
|
||||
b.Run(action, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := buildSignedRequest(b, action)
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("action %s iteration %d: %v", action, i, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── BenchmarkBAPCaller_Discover_Percentiles ───────────────────────────────────
|
||||
// Collects individual request durations and reports p50, p95, and p99 latency
|
||||
// in microseconds via b.ReportMetric. The percentile data is only meaningful
|
||||
// when -benchtime is at least 5s (default used in run_benchmarks.sh).
|
||||
func BenchmarkBAPCaller_Discover_Percentiles(b *testing.B) {
|
||||
durations := make([]time.Duration, 0, b.N)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := buildSignedRequest(b, "discover")
|
||||
start := time.Now()
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("iteration %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
durations = append(durations, time.Since(start))
|
||||
}
|
||||
|
||||
// Compute and report percentiles.
|
||||
if len(durations) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] })
|
||||
|
||||
p50 := durations[len(durations)*50/100]
|
||||
p95 := durations[len(durations)*95/100]
|
||||
p99 := durations[len(durations)*99/100]
|
||||
|
||||
b.ReportMetric(float64(p50.Microseconds()), "p50_µs")
|
||||
b.ReportMetric(float64(p95.Microseconds()), "p95_µs")
|
||||
b.ReportMetric(float64(p99.Microseconds()), "p99_µs")
|
||||
}
|
||||
|
||||
// ── BenchmarkBAPCaller_CacheWarm / CacheCold ─────────────────────────────────
|
||||
// Compares latency when the Redis cache holds a pre-warmed key set (CacheWarm)
|
||||
// vs. when each iteration has a fresh message_id that the cache has never seen
|
||||
// (CacheCold). The delta reveals the key-lookup overhead on a cold path.
|
||||
|
||||
// BenchmarkBAPCaller_CacheWarm sends a fixed body (constant message_id) so the
|
||||
// simplekeymanager's Redis cache is hit on every iteration after the first.
|
||||
func BenchmarkBAPCaller_CacheWarm(b *testing.B) {
|
||||
body := warmFixtureBody(b, "discover")
|
||||
|
||||
// Warm-up: send once to populate the cache before the timer starts.
|
||||
warmReq := buildSignedRequestFixed(b, "discover", body)
|
||||
if err := sendRequest(warmReq); err != nil {
|
||||
b.Fatalf("cache warm-up request failed: %v", err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := buildSignedRequestFixed(b, "discover", body)
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("CacheWarm iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBAPCaller_CacheCold uses a fresh message_id per iteration, so every
|
||||
// request experiences a cache miss and a full key-derivation round-trip.
|
||||
func BenchmarkBAPCaller_CacheCold(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := buildSignedRequest(b, "discover") // fresh IDs each time
|
||||
if err := sendRequest(req); err != nil {
|
||||
b.Errorf("CacheCold iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BenchmarkBAPCaller_RPS ────────────────────────────────────────────────────
|
||||
// Reports requests-per-second as a custom metric alongside the default ns/op.
|
||||
// Run with -benchtime=30s for a stable RPS reading.
|
||||
func BenchmarkBAPCaller_RPS(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
var count int64
|
||||
start := time.Now()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
var local int64
|
||||
for pb.Next() {
|
||||
req := buildSignedRequest(b, "discover")
|
||||
if err := sendRequest(req); err == nil {
|
||||
local++
|
||||
}
|
||||
}
|
||||
// Accumulate without atomic for simplicity — final value only read after
|
||||
// RunParallel returns and all goroutines have exited.
|
||||
count += local
|
||||
})
|
||||
|
||||
elapsed := time.Since(start).Seconds()
|
||||
if elapsed > 0 {
|
||||
rps := float64(count) / elapsed
|
||||
b.ReportMetric(rps, "req/s")
|
||||
fmt.Printf(" RPS: %.0f over %.1fs\n", rps, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// ── helper: one-shot HTTP client ─────────────────────────────────────────────
|
||||
|
||||
// benchHTTPClient is a shared client for all benchmark goroutines.
|
||||
// MaxConnsPerHost caps the total active connections to localhost so we don't
|
||||
// exhaust the OS ephemeral port range. MaxIdleConnsPerHost keeps that many
|
||||
// connections warm in the pool so parallel goroutines reuse them rather than
|
||||
// opening fresh TCP connections on every request.
|
||||
var benchHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 200,
|
||||
MaxIdleConnsPerHost: 200,
|
||||
MaxConnsPerHost: 200,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: true, // no benefit compressing localhost traffic
|
||||
},
|
||||
}
|
||||
13
benchmarks/e2e/keys_test.go
Normal file
13
benchmarks/e2e/keys_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package e2e_bench_test
|
||||
|
||||
// Development key pair from config/local-retail-bap.yaml.
|
||||
// Used across the retail devkit for non-production testing.
|
||||
// DO NOT use in any production or staging environment.
|
||||
const (
|
||||
benchSubscriberID = "sandbox.food-finder.com"
|
||||
benchKeyID = "76EU7VwahYv4XztXJzji9ssiSV74eWXWBcCKGn7jAdm5VGLCdYAJ8j"
|
||||
benchPrivKey = "rrNtVgyASCGlo+ebsJaA37D5CZYZVfT0JA5/vlkTeV0="
|
||||
benchPubKey = "oFIk7KqCqvqRYkLMjQqiaKM5oOozkYT64bfLuc8p/SU="
|
||||
benchEncrPrivKey = "rrNtVgyASCGlo+ebsJaA37D5CZYZVfT0JA5/vlkTeV0="
|
||||
benchEncrPubKey = "oFIk7KqCqvqRYkLMjQqiaKM5oOozkYT64bfLuc8p/SU="
|
||||
)
|
||||
63
benchmarks/e2e/mocks_test.go
Normal file
63
benchmarks/e2e/mocks_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package e2e_bench_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// startMockBPP starts an httptest server that accepts any POST request and
|
||||
// immediately returns a valid Beckn ACK. This replaces the real BPP backend,
|
||||
// isolating benchmark results to adapter-internal latency only.
|
||||
func startMockBPP() *httptest.Server {
|
||||
ackBody := `{"message":{"ack":{"status":"ACK"}}}`
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, ackBody)
|
||||
}))
|
||||
}
|
||||
|
||||
// subscriberRecord mirrors the registry API response shape for a single subscriber.
|
||||
type subscriberRecord struct {
|
||||
SubscriberID string `json:"subscriber_id"`
|
||||
UniqueKeyID string `json:"unique_key_id"`
|
||||
SigningPublicKey string `json:"signing_public_key"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
ValidUntil string `json:"valid_until"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// startMockRegistry starts an httptest server that returns a subscriber record
|
||||
// matching the benchmark test keys. The signvalidator plugin uses this to
|
||||
// resolve the public key for signature verification on incoming requests.
|
||||
func startMockRegistry() *httptest.Server {
|
||||
record := subscriberRecord{
|
||||
SubscriberID: benchSubscriberID,
|
||||
UniqueKeyID: benchKeyID,
|
||||
SigningPublicKey: benchPubKey,
|
||||
ValidFrom: time.Now().AddDate(-1, 0, 0).Format(time.RFC3339),
|
||||
ValidUntil: time.Now().AddDate(10, 0, 0).Format(time.RFC3339),
|
||||
Status: "SUBSCRIBED",
|
||||
}
|
||||
body, _ := json.Marshal([]subscriberRecord{record})
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Support both GET (lookup) and POST (lookup with body) registry calls.
|
||||
// Respond with the subscriber record regardless of subscriber_id query param.
|
||||
subscriberID := r.URL.Query().Get("subscriber_id")
|
||||
if subscriberID == "" {
|
||||
// Try extracting from path for dedi-registry style calls.
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")
|
||||
if len(parts) > 0 {
|
||||
subscriberID = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(body)
|
||||
}))
|
||||
}
|
||||
466
benchmarks/e2e/setup_test.go
Normal file
466
benchmarks/e2e/setup_test.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package e2e_bench_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/beckn-one/beckn-onix/core/module"
|
||||
"github.com/beckn-one/beckn-onix/core/module/handler"
|
||||
"github.com/beckn-one/beckn-onix/pkg/model"
|
||||
"github.com/beckn-one/beckn-onix/pkg/plugin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// Package-level references shared across all benchmarks.
|
||||
var (
|
||||
adapterServer *httptest.Server
|
||||
miniRedis *miniredis.Miniredis
|
||||
mockBPP *httptest.Server
|
||||
mockRegistry *httptest.Server
|
||||
pluginDir string
|
||||
moduleRoot string // set in TestMain; used by buildBAPCallerConfig for local file paths
|
||||
)
|
||||
|
||||
// Plugins to compile for the benchmark. Each entry is (pluginID, source path relative to module root).
|
||||
var pluginsToBuild = []struct {
|
||||
id string
|
||||
src string
|
||||
}{
|
||||
{"router", "pkg/plugin/implementation/router/cmd/plugin.go"},
|
||||
{"signer", "pkg/plugin/implementation/signer/cmd/plugin.go"},
|
||||
{"signvalidator", "pkg/plugin/implementation/signvalidator/cmd/plugin.go"},
|
||||
{"simplekeymanager", "pkg/plugin/implementation/simplekeymanager/cmd/plugin.go"},
|
||||
{"cache", "pkg/plugin/implementation/cache/cmd/plugin.go"},
|
||||
{"schemav2validator", "pkg/plugin/implementation/schemav2validator/cmd/plugin.go"},
|
||||
{"otelsetup", "pkg/plugin/implementation/otelsetup/cmd/plugin.go"},
|
||||
// registry is required by stdHandler to wire KeyManager, even on the caller
|
||||
// path where sign-validation never runs.
|
||||
{"registry", "pkg/plugin/implementation/registry/cmd/plugin.go"},
|
||||
}
|
||||
|
||||
// TestMain is the entry point for the benchmark package. It:
|
||||
// 1. Compiles all required .so plugins into a temp directory
|
||||
// 2. Starts miniredis (in-process Redis)
|
||||
// 3. Starts mock BPP and registry HTTP servers
|
||||
// 4. Starts the adapter as an httptest.Server
|
||||
// 5. Runs all benchmarks
|
||||
// 6. Tears everything down in reverse order
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
|
||||
// ── Step 1: Compile plugins ───────────────────────────────────────────────
|
||||
var err error
|
||||
pluginDir, err = os.MkdirTemp("", "beckn-bench-plugins-*")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to create plugin temp dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.RemoveAll(pluginDir)
|
||||
|
||||
moduleRoot, err = findModuleRoot()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to locate module root: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("=== Building plugins (first run may take 60-90s) ===\n")
|
||||
for _, p := range pluginsToBuild {
|
||||
outPath := filepath.Join(pluginDir, p.id+".so")
|
||||
srcPath := filepath.Join(moduleRoot, p.src)
|
||||
fmt.Printf(" compiling %s.so ...\n", p.id)
|
||||
cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", outPath, srcPath)
|
||||
cmd.Dir = moduleRoot
|
||||
if out, buildErr := cmd.CombinedOutput(); buildErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to build plugin %s:\n%s\n", p.id, string(out))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
fmt.Printf("=== All plugins compiled successfully ===\n\n")
|
||||
|
||||
// ── Step 2: Start miniredis ───────────────────────────────────────────────
|
||||
miniRedis, err = miniredis.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to start miniredis: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer miniRedis.Close()
|
||||
|
||||
// ── Step 3: Start mock servers ────────────────────────────────────────────
|
||||
mockBPP = startMockBPP()
|
||||
defer mockBPP.Close()
|
||||
|
||||
mockRegistry = startMockRegistry()
|
||||
defer mockRegistry.Close()
|
||||
|
||||
// ── Step 4: Start adapter ─────────────────────────────────────────────────
|
||||
adapterServer, err = startAdapter(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to start adapter: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer adapterServer.Close()
|
||||
|
||||
// ── Step 5: Run benchmarks ────────────────────────────────────────────────
|
||||
// Silence the adapter's zerolog output for the duration of the benchmark
|
||||
// run. Without this, every HTTP request the adapter processes emits a JSON
|
||||
// log line to stdout, which interleaves with Go's benchmark result lines
|
||||
// (BenchmarkFoo-N\t\t<count>\t<ns/op>) and makes benchstat unparseable.
|
||||
// Setup logging above still ran normally; zerolog.Disabled is set only here,
|
||||
// just before m.Run(), so errors during startup remain visible.
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// findModuleRoot walks up from the current directory to find the go.mod root.
|
||||
func findModuleRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("go.mod not found from %s", dir)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
// writeRoutingConfig reads the benchmark routing config template, replaces the
|
||||
// BENCH_BPP_URL placeholder with the live mock BPP server URL, and writes the
|
||||
// result to a temp file. Returns the path to the temp file.
|
||||
func writeRoutingConfig(bppURL string) (string, error) {
|
||||
templatePath := filepath.Join("testdata", "routing-BAPCaller.yaml")
|
||||
data, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading routing config template: %w", err)
|
||||
}
|
||||
content := strings.ReplaceAll(string(data), "BENCH_BPP_URL", bppURL)
|
||||
f, err := os.CreateTemp("", "bench-routing-*.yaml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating temp routing config: %w", err)
|
||||
}
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
f.Close()
|
||||
return "", fmt.Errorf("writing routing config: %w", err)
|
||||
}
|
||||
f.Close()
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// startAdapter constructs a fully wired adapter using the compiled plugins and
|
||||
// returns it as an *httptest.Server. All external dependencies are replaced with
|
||||
// local mock servers: Redis → miniredis, BPP → mockBPP, registry → mockRegistry.
|
||||
func startAdapter(ctx context.Context) (*httptest.Server, error) {
|
||||
routingConfigPath, err := writeRoutingConfig(mockBPP.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing routing config: %w", err)
|
||||
}
|
||||
|
||||
// Plugin manager: load all compiled .so files from pluginDir.
|
||||
mgr, closer, err := plugin.NewManager(ctx, &plugin.ManagerConfig{
|
||||
Root: pluginDir,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating plugin manager: %w", err)
|
||||
}
|
||||
_ = closer // closer is called when the server shuts down; deferred in TestMain via server.Close
|
||||
|
||||
// Build module configurations.
|
||||
mCfgs := []module.Config{
|
||||
buildBAPCallerConfig(routingConfigPath, mockRegistry.URL),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
if err := module.Register(ctx, mCfgs, mux, mgr); err != nil {
|
||||
return nil, fmt.Errorf("registering modules: %w", err)
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(mux)
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// buildBAPCallerConfig returns the module.Config for the bapTxnCaller handler,
|
||||
// mirroring config/local-retail-bap.yaml but pointing at benchmark mock services.
|
||||
// registryURL must point at the mock registry so simplekeymanager can satisfy the
|
||||
// Registry requirement imposed by stdHandler — even though the caller path never
|
||||
// performs signature validation, the handler wiring requires it to be present.
|
||||
func buildBAPCallerConfig(routingConfigPath, registryURL string) module.Config {
|
||||
return module.Config{
|
||||
Name: "bapTxnCaller",
|
||||
Path: "/bap/caller/",
|
||||
Handler: handler.Config{
|
||||
Type: handler.HandlerTypeStd,
|
||||
Role: model.RoleBAP,
|
||||
SubscriberID: benchSubscriberID,
|
||||
HttpClientConfig: handler.HttpClientConfig{
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 200,
|
||||
IdleConnTimeout: 300 * time.Second,
|
||||
ResponseHeaderTimeout: 5 * time.Second,
|
||||
},
|
||||
Plugins: handler.PluginCfg{
|
||||
// Registry is required by stdHandler before it will wire KeyManager,
|
||||
// even on the caller path where sign-validation never runs. We point
|
||||
// it at the mock registry (retry_max=0 so failures are immediate).
|
||||
Registry: &plugin.Config{
|
||||
ID: "registry",
|
||||
Config: map[string]string{
|
||||
"url": registryURL,
|
||||
"retry_max": "0",
|
||||
},
|
||||
},
|
||||
KeyManager: &plugin.Config{
|
||||
ID: "simplekeymanager",
|
||||
Config: map[string]string{
|
||||
"networkParticipant": benchSubscriberID,
|
||||
"keyId": benchKeyID,
|
||||
"signingPrivateKey": benchPrivKey,
|
||||
"signingPublicKey": benchPubKey,
|
||||
"encrPrivateKey": benchEncrPrivKey,
|
||||
"encrPublicKey": benchEncrPubKey,
|
||||
},
|
||||
},
|
||||
SchemaValidator: &plugin.Config{
|
||||
ID: "schemav2validator",
|
||||
Config: map[string]string{
|
||||
"type": "file",
|
||||
"location": filepath.Join(moduleRoot, "benchmarks/e2e/testdata/beckn.yaml"),
|
||||
"cacheTTL": "3600",
|
||||
},
|
||||
},
|
||||
Cache: &plugin.Config{
|
||||
ID: "cache",
|
||||
Config: map[string]string{
|
||||
"addr": miniRedis.Addr(),
|
||||
},
|
||||
},
|
||||
Router: &plugin.Config{
|
||||
ID: "router",
|
||||
Config: map[string]string{
|
||||
"routingConfig": routingConfigPath,
|
||||
},
|
||||
},
|
||||
Signer: &plugin.Config{
|
||||
ID: "signer",
|
||||
},
|
||||
},
|
||||
Steps: []string{"addRoute", "sign", "validateSchema"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── T7: Request builder and Beckn signing helper ──────────────────────────────
|
||||
|
||||
// becknPayloadTemplate holds the raw JSON for a fixture file with sentinels.
|
||||
var fixtureCache = map[string][]byte{}
|
||||
|
||||
// loadFixture reads a fixture file from testdata/ and caches it.
|
||||
func loadFixture(action string) ([]byte, error) {
|
||||
if data, ok := fixtureCache[action]; ok {
|
||||
return data, nil
|
||||
}
|
||||
path := filepath.Join("testdata", action+"_request.json")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading fixture %s: %w", action, err)
|
||||
}
|
||||
fixtureCache[action] = data
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// buildSignedRequest reads the fixture for the given action, substitutes
|
||||
// BENCH_TIMESTAMP / BENCH_MESSAGE_ID / BENCH_TRANSACTION_ID with fresh values,
|
||||
// signs the body using the Beckn Ed25519 spec, and returns a ready-to-send
|
||||
// *http.Request targeting the adapter's /bap/caller/<action> path.
|
||||
func buildSignedRequest(tb testing.TB, action string) *http.Request {
|
||||
tb.Helper()
|
||||
|
||||
fixture, err := loadFixture(action)
|
||||
if err != nil {
|
||||
tb.Fatalf("buildSignedRequest: %v", err)
|
||||
}
|
||||
|
||||
// Substitute sentinels with fresh values for this iteration.
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
msgID := uuid.New().String()
|
||||
txnID := uuid.New().String()
|
||||
|
||||
body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte(now))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte(msgID))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte(txnID))
|
||||
|
||||
// Sign the body per the Beckn Ed25519 spec.
|
||||
authHeader, err := signBecknPayload(body)
|
||||
if err != nil {
|
||||
tb.Fatalf("buildSignedRequest: signing failed: %v", err)
|
||||
}
|
||||
|
||||
url := adapterServer.URL + "/bap/caller/" + action
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
tb.Fatalf("buildSignedRequest: http.NewRequest: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(model.AuthHeaderSubscriber, authHeader)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// buildSignedRequestFixed builds a signed request with a fixed body (same
|
||||
// message_id every call) — used for cache-warm benchmarks.
|
||||
func buildSignedRequestFixed(tb testing.TB, action string, body []byte) *http.Request {
|
||||
tb.Helper()
|
||||
|
||||
authHeader, err := signBecknPayload(body)
|
||||
if err != nil {
|
||||
tb.Fatalf("buildSignedRequestFixed: signing failed: %v", err)
|
||||
}
|
||||
|
||||
url := adapterServer.URL + "/bap/caller/" + action
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
tb.Fatalf("buildSignedRequestFixed: http.NewRequest: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(model.AuthHeaderSubscriber, authHeader)
|
||||
return req
|
||||
}
|
||||
|
||||
// signBecknPayload signs a request body using the Beckn Ed25519 signing spec
|
||||
// and returns a formatted Authorization header value.
|
||||
//
|
||||
// Beckn signing spec:
|
||||
// 1. Digest: "BLAKE-512=" + base64(blake2b-512(body))
|
||||
// 2. Signing string: "(created): <ts>\n(expires): <ts+5m>\ndigest: <digest>"
|
||||
// 3. Signature: base64(ed25519.Sign(privKey, signingString))
|
||||
// 4. Header: Signature keyId="<sub>|<keyId>|ed25519",algorithm="ed25519",
|
||||
// created="<ts>",expires="<ts+5m>",headers="(created) (expires) digest",
|
||||
// signature="<sig>"
|
||||
//
|
||||
// Reference: pkg/plugin/implementation/signer/signer.go
|
||||
func signBecknPayload(body []byte) (string, error) {
|
||||
createdAt := time.Now().Unix()
|
||||
expiresAt := time.Now().Add(5 * time.Minute).Unix()
|
||||
|
||||
// Step 1: BLAKE-512 digest.
|
||||
hasher, _ := blake2b.New512(nil)
|
||||
hasher.Write(body)
|
||||
digest := "BLAKE-512=" + base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
// Step 2: Signing string.
|
||||
signingString := fmt.Sprintf("(created): %d\n(expires): %d\ndigest: %s", createdAt, expiresAt, digest)
|
||||
|
||||
// Step 3: Ed25519 signature.
|
||||
privKeyBytes, err := base64.StdEncoding.DecodeString(benchPrivKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding private key: %w", err)
|
||||
}
|
||||
privKey := ed25519.NewKeyFromSeed(privKeyBytes)
|
||||
sig := base64.StdEncoding.EncodeToString(ed25519.Sign(privKey, []byte(signingString)))
|
||||
|
||||
// Step 4: Format Authorization header (matches generateAuthHeader in step.go).
|
||||
header := fmt.Sprintf(
|
||||
`Signature keyId="%s|%s|ed25519",algorithm="ed25519",created="%d",expires="%d",headers="(created) (expires) digest",signature="%s"`,
|
||||
benchSubscriberID, benchKeyID, createdAt, expiresAt, sig,
|
||||
)
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// warmFixtureBody returns a fixed body for the given action with stable IDs —
|
||||
// used to pre-warm the cache so cache-warm benchmarks hit the Redis fast path.
|
||||
func warmFixtureBody(tb testing.TB, action string) []byte {
|
||||
tb.Helper()
|
||||
fixture, err := loadFixture(action)
|
||||
if err != nil {
|
||||
tb.Fatalf("warmFixtureBody: %v", err)
|
||||
}
|
||||
body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte("2025-01-01T00:00:00Z"))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte("00000000-warm-0000-0000-000000000000"))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte("00000000-warm-txn-0000-000000000000"))
|
||||
return body
|
||||
}
|
||||
|
||||
// sendRequest executes an HTTP request using the shared bench client and
|
||||
// discards the response body. Returns a non-nil error for non-2xx responses.
|
||||
func sendRequest(req *http.Request) error {
|
||||
resp, err := benchHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http do: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Drain the body so the connection is returned to the pool for reuse.
|
||||
// Without this, Go discards the connection after each request, causing
|
||||
// port exhaustion under parallel load ("can't assign requested address").
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
// We accept any 2xx response (ACK or forwarded BPP response).
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── TestSignBecknPayload: validation test before running benchmarks ───────────
|
||||
// Sends a signed discover request to the live adapter and asserts a 200 response,
|
||||
// confirming the signing helper produces headers accepted by the adapter pipeline.
|
||||
func TestSignBecknPayload(t *testing.T) {
|
||||
if adapterServer == nil {
|
||||
t.Skip("adapterServer not initialised (run via TestMain)")
|
||||
}
|
||||
fixture, err := loadFixture("discover")
|
||||
if err != nil {
|
||||
t.Fatalf("loading fixture: %v", err)
|
||||
}
|
||||
|
||||
// Substitute sentinels.
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte(now))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte(uuid.New().String()))
|
||||
body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte(uuid.New().String()))
|
||||
|
||||
authHeader, err := signBecknPayload(body)
|
||||
if err != nil {
|
||||
t.Fatalf("signBecknPayload: %v", err)
|
||||
}
|
||||
|
||||
url := adapterServer.URL + "/bap/caller/discover"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("http.NewRequest: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(model.AuthHeaderSubscriber, authHeader)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("sending request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
t.Logf("Response status: %d, body: %v", resp.StatusCode, result)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected 200 OK, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
3380
benchmarks/e2e/testdata/beckn.yaml
vendored
Normal file
3380
benchmarks/e2e/testdata/beckn.yaml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
84
benchmarks/e2e/testdata/confirm_request.json
vendored
Normal file
84
benchmarks/e2e/testdata/confirm_request.json
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"context": {
|
||||
"action": "confirm",
|
||||
"bapId": "sandbox.food-finder.com",
|
||||
"bapUri": "http://bench-bap.example.com",
|
||||
"bppId": "bench-bpp.example.com",
|
||||
"bppUri": "BENCH_BPP_URL",
|
||||
"messageId": "BENCH_MESSAGE_ID",
|
||||
"transactionId": "BENCH_TRANSACTION_ID",
|
||||
"timestamp": "BENCH_TIMESTAMP",
|
||||
"ttl": "PT30S",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"message": {
|
||||
"order": {
|
||||
"provider": {
|
||||
"id": "bench-provider-001"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": "bench-item-001",
|
||||
"quantity": {
|
||||
"selected": {
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"billing": {
|
||||
"name": "Bench User",
|
||||
"address": "123 Bench Street, Bangalore, 560001",
|
||||
"city": {
|
||||
"name": "Bangalore"
|
||||
},
|
||||
"state": {
|
||||
"name": "Karnataka"
|
||||
},
|
||||
"country": {
|
||||
"code": "IND"
|
||||
},
|
||||
"area_code": "560001",
|
||||
"email": "bench@example.com",
|
||||
"phone": "9999999999"
|
||||
},
|
||||
"fulfillments": [
|
||||
{
|
||||
"id": "f1",
|
||||
"type": "Delivery",
|
||||
"stops": [
|
||||
{
|
||||
"type": "end",
|
||||
"location": {
|
||||
"gps": "12.9716,77.5946",
|
||||
"area_code": "560001"
|
||||
},
|
||||
"contact": {
|
||||
"phone": "9999999999",
|
||||
"email": "bench@example.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"customer": {
|
||||
"person": {
|
||||
"name": "Bench User"
|
||||
},
|
||||
"contact": {
|
||||
"phone": "9999999999",
|
||||
"email": "bench@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"type": "ON-FULFILLMENT",
|
||||
"params": {
|
||||
"amount": "150.00",
|
||||
"currency": "INR"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
17
benchmarks/e2e/testdata/discover_request.json
vendored
Normal file
17
benchmarks/e2e/testdata/discover_request.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"context": {
|
||||
"action": "discover",
|
||||
"bapId": "sandbox.food-finder.com",
|
||||
"bapUri": "http://bench-bap.example.com",
|
||||
"messageId": "BENCH_MESSAGE_ID",
|
||||
"transactionId": "BENCH_TRANSACTION_ID",
|
||||
"timestamp": "BENCH_TIMESTAMP",
|
||||
"ttl": "PT30S",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"message": {
|
||||
"intent": {
|
||||
"textSearch": "pizza"
|
||||
}
|
||||
}
|
||||
}
|
||||
80
benchmarks/e2e/testdata/init_request.json
vendored
Normal file
80
benchmarks/e2e/testdata/init_request.json
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"context": {
|
||||
"action": "init",
|
||||
"bapId": "sandbox.food-finder.com",
|
||||
"bapUri": "http://bench-bap.example.com",
|
||||
"bppId": "bench-bpp.example.com",
|
||||
"bppUri": "BENCH_BPP_URL",
|
||||
"messageId": "BENCH_MESSAGE_ID",
|
||||
"transactionId": "BENCH_TRANSACTION_ID",
|
||||
"timestamp": "BENCH_TIMESTAMP",
|
||||
"ttl": "PT30S",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"message": {
|
||||
"order": {
|
||||
"provider": {
|
||||
"id": "bench-provider-001"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": "bench-item-001",
|
||||
"quantity": {
|
||||
"selected": {
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"billing": {
|
||||
"name": "Bench User",
|
||||
"address": "123 Bench Street, Bangalore, 560001",
|
||||
"city": {
|
||||
"name": "Bangalore"
|
||||
},
|
||||
"state": {
|
||||
"name": "Karnataka"
|
||||
},
|
||||
"country": {
|
||||
"code": "IND"
|
||||
},
|
||||
"area_code": "560001",
|
||||
"email": "bench@example.com",
|
||||
"phone": "9999999999"
|
||||
},
|
||||
"fulfillments": [
|
||||
{
|
||||
"id": "f1",
|
||||
"type": "Delivery",
|
||||
"stops": [
|
||||
{
|
||||
"type": "end",
|
||||
"location": {
|
||||
"gps": "12.9716,77.5946",
|
||||
"area_code": "560001"
|
||||
},
|
||||
"contact": {
|
||||
"phone": "9999999999",
|
||||
"email": "bench@example.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"customer": {
|
||||
"person": {
|
||||
"name": "Bench User"
|
||||
},
|
||||
"contact": {
|
||||
"phone": "9999999999",
|
||||
"email": "bench@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"type": "ON-FULFILLMENT"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
13
benchmarks/e2e/testdata/routing-BAPCaller.yaml
vendored
Normal file
13
benchmarks/e2e/testdata/routing-BAPCaller.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Routing config for v2.0.0 benchmark. Domain is not required for v2.x.x — the
|
||||
# router ignores it and routes purely by version + endpoint.
|
||||
# BENCH_BPP_URL is substituted at runtime with the mock BPP server URL.
|
||||
routingRules:
|
||||
- version: "2.0.0"
|
||||
targetType: "url"
|
||||
target:
|
||||
url: "BENCH_BPP_URL"
|
||||
endpoints:
|
||||
- discover
|
||||
- select
|
||||
- init
|
||||
- confirm
|
||||
55
benchmarks/e2e/testdata/select_request.json
vendored
Normal file
55
benchmarks/e2e/testdata/select_request.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"context": {
|
||||
"action": "select",
|
||||
"bapId": "sandbox.food-finder.com",
|
||||
"bapUri": "http://bench-bap.example.com",
|
||||
"bppId": "bench-bpp.example.com",
|
||||
"bppUri": "BENCH_BPP_URL",
|
||||
"messageId": "BENCH_MESSAGE_ID",
|
||||
"transactionId": "BENCH_TRANSACTION_ID",
|
||||
"timestamp": "BENCH_TIMESTAMP",
|
||||
"ttl": "PT30S",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"message": {
|
||||
"order": {
|
||||
"provider": {
|
||||
"id": "bench-provider-001"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": "bench-item-001",
|
||||
"quantity": {
|
||||
"selected": {
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"fulfillments": [
|
||||
{
|
||||
"id": "f1",
|
||||
"type": "Delivery",
|
||||
"stops": [
|
||||
{
|
||||
"type": "end",
|
||||
"location": {
|
||||
"gps": "12.9716,77.5946",
|
||||
"area_code": "560001"
|
||||
},
|
||||
"contact": {
|
||||
"phone": "9999999999",
|
||||
"email": "bench@example.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"type": "ON-FULFILLMENT"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user