diff --git a/.github/workflows/antithesis.yml b/.github/workflows/antithesis.yml new file mode 100644 index 0000000000..f95fe24779 --- /dev/null +++ b/.github/workflows/antithesis.yml @@ -0,0 +1,59 @@ +name: Antithesis Integration +on: + workflow_dispatch: + inputs: + run_tests: + description: 'Run Antithesis tests after pushing images' + type: boolean + default: true + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'test/antithesis/**' + +jobs: + push-antithesis-images: + name: Push Images to Antithesis + runs-on: shipfox-4vcpu-ubuntu-2404 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Earthly + uses: earthly/actions-setup@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + version: "latest" + + - name: Setup Environment + uses: ./.github/actions/default + with: + token: ${{ secrets.NUMARY_GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Antithesis Docker Registry + run: | + echo '${{ secrets.ANTITHESIS_JSON_KEY }}' | docker login -u _json_key https://us-central1-docker.pkg.dev --password-stdin + + - name: Build and Push Config Image + run: | + cd test/antithesis + earthly --push +requirements-build --ANTITHESIS_REPOSITORY=${{ secrets.ANTITHESIS_REPOSITORY }} + + - name: Run Antithesis Tests + if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_tests == true }} + run: | + cd test/antithesis + earthly +run --ANTITHESIS_SLACK_REPORT_RECIPIENT="${{ secrets.ANTITHESIS_SLACK_REPORT_RECIPIENT }}" --ANTITHESIS_REPOSITORY=${{ secrets.ANTITHESIS_REGISTRY }} --ANTITHESIS_PASSWORD=${{ secrets.ANTITHESIS_PASSWORD }} + # env: + # ANTITHESIS_PASSWORD: ${{ secrets.ANTITHESIS_PASSWORD }} \ No newline at end of file diff --git a/go.mod b/go.mod index b0c567948f..722c7e6ff1 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ replace google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215 => google. require ( github.com/ThreeDotsLabs/watermill v1.4.6 github.com/alitto/pond v1.9.2 + github.com/antithesishq/antithesis-sdk-go v0.4.3 github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 diff --git a/go.sum b/go.sum index b298df17a8..e8e7c5d731 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= +github.com/antithesishq/antithesis-sdk-go v0.4.3 h1:a2hGdDogClzHzFu20r1z0tzD6zwSWUipiaerAjZVP90= +github.com/antithesishq/antithesis-sdk-go v0.4.3/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 549dd561ef..7b557176ff 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -4,13 +4,15 @@ import ( "context" "errors" "fmt" + "sync" + "time" + + "github.com/antithesishq/antithesis-sdk-go/assert" "github.com/formancehq/ledger/internal/storage/common" systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/formancehq/ledger/internal/tracing" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" - "sync" - "time" "github.com/alitto/pond" "github.com/formancehq/go-libs/v3/bun/bunpaginate" @@ -35,13 +37,31 @@ type Driver struct { parallelBucketMigrations int } -func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgerstore.Store, error) { +/* +CreateLedger creates a new ledger in the system and sets up all necessary database objects. +The function follows these steps: + 1. Create a ledger record in the system store (_system.ledgers table) + 2. Get the bucket (database schema) for this ledger + 3. Check if the bucket is already initialized: + a. If initialized: Verify it's up to date and add ledger-specific objects to it + b. If not initialized: Create the bucket schema with all necessary tables + 4. Return a ledger store that provides an interface to interact with the ledger + +Note: This entire process is wrapped in a database transaction, ensuring atomicity. +If any step fails, the entire transaction is rolled back, preventing partial state. +*/ +func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgerstore.Store, error) { var ret *ledgerstore.Store + + // Run the entire ledger creation process in a transaction for atomicity + // This ensures that either all steps succeed or none do (preventing partial state) err := d.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { systemStore := d.systemStoreFactory.Create(tx) + // Step 1: Create the ledger record in the system store if err := systemStore.CreateLedger(ctx, l); err != nil { + // Handle the case where the ledger already exists if errors.Is(postgres.ResolveError(err), postgres.ErrConstraintsFailed{}) { return systemcontroller.ErrLedgerAlreadyExists } @@ -53,6 +73,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto if err != nil { return fmt.Errorf("checking if bucket is initialized: %w", err) } + if isInitialized { upToDate, err := b.IsUpToDate(ctx, tx) if err != nil { @@ -60,6 +81,18 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } if !upToDate { + assert.AlwaysOrUnreachable( + // @todo: replace this with a proper flag detailing wether we're + // operating a new version of the binary or not. + // if we are, we are definitely expecting this to happen. + // if we're not, this should be unreachable. + false, + "Bucket is outdated", + map[string]any{ + "bucket": l.Bucket, + }, + ) + return systemcontroller.ErrBucketOutdated } @@ -72,14 +105,18 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } } + // Step 4: Create a store for interacting with the ledger ret = d.ledgerStoreFactory.Create(b, *l) return nil }) + + // If any error occurred during the transaction, resolve and return it if err != nil { return nil, postgres.ResolveError(err) } + // Return the created ledger store return ret, nil } diff --git a/pkg/client/Earthfile b/pkg/client/Earthfile new file mode 100644 index 0000000000..a17835da3f --- /dev/null +++ b/pkg/client/Earthfile @@ -0,0 +1,8 @@ +VERSION 0.7 + +FROM --platform=linux/amd64 golang:1.22.2 + +client: + FROM scratch + COPY . /client + SAVE ARTIFACT /client client \ No newline at end of file diff --git a/test/antithesis/Earthfile b/test/antithesis/Earthfile new file mode 100644 index 0000000000..0052105553 --- /dev/null +++ b/test/antithesis/Earthfile @@ -0,0 +1,78 @@ +VERSION 0.8 + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +run: + WAIT + BUILD +requirements-build + END + + FROM curlimages/curl + ARG ANTITHESIS_USERNAME=formance + ARG --required ANTITHESIS_SLACK_REPORT_RECIPIENT + RUN --no-cache --secret ANTITHESIS_PASSWORD curl \ + --fail \ + --user "$ANTITHESIS_USERNAME:$ANTITHESIS_PASSWORD" \ + -X POST https://formance.antithesis.com/api/v1/launch_experiment/formance -d "{ + \"params\": { + \"custom.duration\": \"0.1\", + \"antithesis.report.recipients\": \"${ANTITHESIS_SLACK_REPORT_RECIPIENT}\", + \"antithesis.config_image\": \"antithesis-config:latest\", + \"antithesis.images\": \"ledger:latest;workload:latest\" + } + }" + +run-1h: + WAIT + BUILD +requirements-build + END + + FROM curlimages/curl + ARG ANTITHESIS_USERNAME=formance + ARG --required ANTITHESIS_SLACK_REPORT_RECIPIENT + RUN --no-cache --secret ANTITHESIS_PASSWORD curl \ + --fail \ + --user "$ANTITHESIS_USERNAME:$ANTITHESIS_PASSWORD" \ + -X POST https://formance.antithesis.com/api/v1/launch_experiment/formance -d "{ + \"params\": { + \"custom.duration\": \"1\", + \"antithesis.report.recipients\": \"${ANTITHESIS_SLACK_REPORT_RECIPIENT}\", + \"antithesis.config_image\": \"antithesis-config:latest\", + \"antithesis.images\": \"ledger:latest;workload:latest\" + } + }" + +debugger: + FROM curlimages/curl + + ARG ANTITHESIS_USERNAME=formance + ARG --required ANTITHESIS_SLACK_REPORT_RECIPIENT + ARG --required VTIME + ARG --required SESSION_ID + ARG --required HASH + RUN echo "{ + \"params\": { + \"antithesis.debugging.session_id\": \"${SESSION_ID}\", + \"antithesis.debugging.input_hash\": \"${HASH}\", + \"antithesis.debugging.vtime\": \"${VTIME}\", + \"antithesis.report.recipients\": \"${ANTITHESIS_SLACK_REPORT_RECIPIENT}\" + } + }" > /tmp/debug_params.json + + # Display the debug parameters for verification + RUN cat /tmp/debug_params.json + + RUN --no-cache --secret ANTITHESIS_PASSWORD curl \ + --fail \ + --user "$ANTITHESIS_USERNAME:$ANTITHESIS_PASSWORD" \ + -X POST https://formance.antithesis.com/api/v1/launch/debugging \ + -d @/tmp/debug_params.json + +requirements-build: + ARG --required ANTITHESIS_REPOSITORY + + BUILD --pass-args ./config+build + BUILD --pass-args ./image+build + BUILD --pass-args ./workload+build \ No newline at end of file diff --git a/test/antithesis/config/Earthfile b/test/antithesis/config/Earthfile new file mode 100644 index 0000000000..201f9c5094 --- /dev/null +++ b/test/antithesis/config/Earthfile @@ -0,0 +1,11 @@ +VERSION 0.8 + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +build: + FROM --platform=linux/amd64 scratch + ARG --required ANTITHESIS_REPOSITORY + COPY docker-compose.yml /docker-compose.yml + COPY --dir gateway /gateway + + SAVE IMAGE --push --no-manifest-list ${ANTITHESIS_REPOSITORY}/antithesis-config:latest \ No newline at end of file diff --git a/test/antithesis/config/docker-compose.yml b/test/antithesis/config/docker-compose.yml new file mode 100644 index 0000000000..8de28914a9 --- /dev/null +++ b/test/antithesis/config/docker-compose.yml @@ -0,0 +1,88 @@ +--- +networks: + formance: + driver: bridge + ipam: + config: + - subnet: 10.0.29.0/24 + +services: + postgres: + image: "postgres:15-alpine" + hostname: postgres + container_name: postgres + command: + - -c + - max_connections=100 + environment: + POSTGRES_USER: "ledger" + POSTGRES_PASSWORD: "ledger" + POSTGRES_DB: "ledger" + PGDATA: /data/postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ledger" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + formance: + ipv4_address: 10.0.29.16 + + ledger1: + image: "us-central1-docker.pkg.dev/molten-verve-216720/formance-repository/ledger:latest" + hostname: ledger1 + container_name: ledger1 + environment: + POSTGRES_URI: "postgresql://ledger:ledger@10.0.29.16:5432/ledger?sslmode=disable" + BIND: ":8080" + POSTGRES_CONN_MAX_IDLE_TIME: "1m0s" + POSTGRES_MAX_IDLE_CONNS: "20" + POSTGRES_MAX_OPEN_CONNS: "20" + networks: + formance: + ipv4_address: 10.0.29.17 + depends_on: + postgres: + condition: service_healthy + + ledger2: + image: "us-central1-docker.pkg.dev/molten-verve-216720/formance-repository/ledger:latest" + hostname: ledger2 + container_name: ledger2 + environment: + POSTGRES_URI: "postgresql://ledger:ledger@10.0.29.16:5432/ledger?sslmode=disable" + POSTGRES_CONN_MAX_IDLE_TIME: "1m0s" + POSTGRES_MAX_IDLE_CONNS: "20" + POSTGRES_MAX_OPEN_CONNS: "20" + BIND: ":8080" + networks: + formance: + ipv4_address: 10.0.29.18 + depends_on: + postgres: + condition: service_healthy + + workload: + image: "us-central1-docker.pkg.dev/molten-verve-216720/formance-repository/workload:latest" + hostname: workload + container_name: workload + networks: + formance: + ipv4_address: 10.0.29.19 + depends_on: + - gateway + + gateway: + image: "ghcr.io/formancehq/gateway:v2.0.0-rc.20" + hostname: gateway + container_name: gateway + volumes: + - ./gateway/Caddyfile:/etc/caddy/Caddyfile + ports: + - "8080:8080" + networks: + formance: + ipv4_address: 10.0.29.20 + depends_on: + - ledger1 + - ledger2 \ No newline at end of file diff --git a/test/antithesis/config/gateway/Caddyfile b/test/antithesis/config/gateway/Caddyfile new file mode 100644 index 0000000000..95dd650008 --- /dev/null +++ b/test/antithesis/config/gateway/Caddyfile @@ -0,0 +1,12 @@ +{ + # Local env dev config + auto_https off + debug +} + +:8080 { + reverse_proxy { + to ledger1:8080 ledger2:8080 + lb_policy first + } +} diff --git a/test/antithesis/config/kube.yaml b/test/antithesis/config/kube.yaml new file mode 100644 index 0000000000..e6b6497229 --- /dev/null +++ b/test/antithesis/config/kube.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: workload + namespace: formance-dev + labels: + app: workload +spec: + containers: + - name: workload + image: us-central1-docker.pkg.dev/molten-verve-216720/formance-repository/workload:latest + imagePullPolicy: Always \ No newline at end of file diff --git a/test/antithesis/image/Earthfile b/test/antithesis/image/Earthfile new file mode 100644 index 0000000000..118d292f6b --- /dev/null +++ b/test/antithesis/image/Earthfile @@ -0,0 +1,44 @@ +VERSION 0.8 + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +CACHE --sharing=shared --id go-mod-cache /go/pkg/mod +CACHE --sharing=shared --id go-cache /root/.cache/go-build + +compile: + FROM --platform=linux/amd64 golang:1.23 + CACHE --sharing=shared --id go-mod-cache /go/pkg/mod + CACHE --sharing=shared --id go-cache /root/.cache/go-build + + COPY ../../..+sources/src /src + RUN go install github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor@latest + WORKDIR /src + + RUN go mod download + RUN mkdir -p /ledger_instrumented + RUN /go/bin/antithesis-go-instrumentor . /ledger_instrumented + + WORKDIR /ledger_instrumented/customer + RUN go build -race -o ledger + + SAVE ARTIFACT /ledger_instrumented/customer/ledger + SAVE ARTIFACT /ledger_instrumented/symbols + +build: + FROM --platform=linux/amd64 ubuntu:latest + RUN apt-get update -y && apt-get install -y postgresql-client curl + + COPY (+compile/ledger) /bin/ledger + COPY (+compile/symbols) /symbols + COPY entrypoint.sh /bin/entrypoint.sh + + RUN chmod 777 /bin/entrypoint.sh + + ENTRYPOINT ["/bin/entrypoint.sh"] + EXPOSE 8080 + + ARG --required ANTITHESIS_REPOSITORY + + SAVE IMAGE --push --no-manifest-list "${ANTITHESIS_REPOSITORY}/ledger:latest" \ No newline at end of file diff --git a/test/antithesis/image/entrypoint.sh b/test/antithesis/image/entrypoint.sh new file mode 100644 index 0000000000..37973d9470 --- /dev/null +++ b/test/antithesis/image/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# make sure pg is ready to accept connections +until pg_isready -d ledger -h 10.0.29.16 -U ledger +do + echo "Waiting for postgres at: $POSTGRES_URI" + sleep 2; +done + +echo "Postgres is ready; serving ledger!" + +ledger serve diff --git a/test/antithesis/workload/Earthfile b/test/antithesis/workload/Earthfile new file mode 100644 index 0000000000..4dfef2d51b --- /dev/null +++ b/test/antithesis/workload/Earthfile @@ -0,0 +1,46 @@ +VERSION 0.7 + +FROM --platform=linux/amd64 golang:1.22.2 + +CACHE --sharing=shared --id go-mod-cache /go/pkg/mod +CACHE --sharing=shared --id go-cache /root/.cache/go-build + +sources: + WORKDIR /src/test/antithesis/workload + COPY go.* . + COPY --dir bin internal . + + SAVE ARTIFACT /src + +compile: + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + + RUN go install github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor@latest + + COPY +sources/src /src + + WORKDIR /src/test/antithesis/workload + COPY --dir ../../../pkg/client+client/client ../../../pkg/client + RUN mkdir -p /workload_instrumented + RUN mkdir -p /out/cmds + + RUN antithesis-go-instrumentor -assert_only . + RUN go build -o /out/init ./bin/init + + FOR file IN $(ls bin/cmds) + RUN go build -o /out/cmds/$file ./bin/cmds/$file + END + + SAVE ARTIFACT /out + +build: + FROM --platform=linux/amd64 ubuntu:latest + COPY (+compile/out/init) /init + ENTRYPOINT ["/init"] + + COPY (+compile/out/cmds/*) /opt/antithesis/test/v1/main/ + + ARG --required ANTITHESIS_REPOSITORY + + SAVE IMAGE --push --no-manifest-list ${ANTITHESIS_REPOSITORY}/workload:latest \ No newline at end of file diff --git a/test/antithesis/workload/bin/cmds/anytime_version_upgrade/main.go b/test/antithesis/workload/bin/cmds/anytime_version_upgrade/main.go new file mode 100644 index 0000000000..93ed37a6f2 --- /dev/null +++ b/test/antithesis/workload/bin/cmds/anytime_version_upgrade/main.go @@ -0,0 +1,8 @@ +package main + +import "log" + +func main() { + // TODO: implement + log.Println("placeholder command for anytime_version_upgrade") +} diff --git a/test/antithesis/workload/bin/cmds/eventually_correct/main.go b/test/antithesis/workload/bin/cmds/eventually_correct/main.go new file mode 100644 index 0000000000..7a5ce3e022 --- /dev/null +++ b/test/antithesis/workload/bin/cmds/eventually_correct/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/big" + "os" + "sync" + + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" + "github.com/formancehq/ledger/test/antithesis/internal" +) + +func main() { + log.Println("composer: eventually_correct") + ctx := context.Background() + client := internal.NewClient() + + ledgers, err := client.Ledger.V2.ListLedgers(ctx, operations.V2ListLedgersRequest{}) + + if err != nil { + log.Printf("error listing ledgers: %s", err) + os.Exit(1) + } + + wg := sync.WaitGroup{} + for _, ledger := range ledgers.V2LedgerListResponse.Cursor.Data { + wg.Add(1) + go func(ledger string) { + defer wg.Done() + checkVolumes(ctx, client, ledger) + }(ledger.Name) + } + wg.Wait() +} + +func checkVolumes(ctx context.Context, client *client.Formance, ledger string) { + aggregated, err := client.Ledger.V2.GetBalancesAggregated(ctx, operations.V2GetBalancesAggregatedRequest{ + Ledger: ledger, + }) + if err != nil { + if internal.IsServerError(aggregated.GetHTTPMeta()) { + assert.Always( + false, + fmt.Sprintf("error getting aggregated balances for ledger %s: %s", ledger, err), + internal.Details{ + "error": err, + }, + ) + } else { + log.Fatalf("error getting aggregated balances for ledger %s: %s", ledger, err) + } + } + + for asset, volumes := range aggregated.V2AggregateBalancesResponse.Data { + assert.Always( + volumes.Cmp(new(big.Int)) == 0, + fmt.Sprintf("aggregated volumes for asset %s should be 0", + asset, + ), internal.Details{ + "error": err, + }) + } + + log.Printf("composer: eventually_correct: done for ledger %s", ledger) +} + +func IsServerError(httpMeta components.HTTPMetadata) bool { + return httpMeta.Response.StatusCode >= 400 && httpMeta.Response.StatusCode < 600 +} diff --git a/test/antithesis/workload/bin/cmds/first_default_ledger/main.go b/test/antithesis/workload/bin/cmds/first_default_ledger/main.go new file mode 100644 index 0000000000..b609ed331d --- /dev/null +++ b/test/antithesis/workload/bin/cmds/first_default_ledger/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "log" + + "github.com/formancehq/ledger/test/antithesis/internal" +) + +func main() { + log.Println("composer: first_default_ledger") + + ctx := context.Background() + client := internal.NewClient() + ledger := "default" + + _, err := internal.CreateLedger( + ctx, + client, + ledger, + ledger, + ) + + if err != nil { + log.Fatalf("error creating ledger %s: %s", ledger, err) + } + + log.Println("composer: first_default_ledger: done") +} diff --git a/test/antithesis/workload/bin/cmds/parallel_driver_ledger_create/main.go b/test/antithesis/workload/bin/cmds/parallel_driver_ledger_create/main.go new file mode 100644 index 0000000000..4a3c649361 --- /dev/null +++ b/test/antithesis/workload/bin/cmds/parallel_driver_ledger_create/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/formancehq/ledger/test/antithesis/internal" +) + +func main() { + log.Println("composer: parallel_driver_ledger_create") + ctx := context.Background() + client := internal.NewClient() + id := rand.Intn(1e6) + ledger := fmt.Sprintf("ledger-%d", id) + + res, err := internal.CreateLedger( + ctx, + client, + ledger, + ledger, + ) + + assert.Sometimes(err == nil, "ledger should have been created properly", internal.Details{ + "error": err, + }) + + assert.Always( + !internal.IsServerError(res.GetHTTPMeta()), + "no internal server error when creating ledger", + internal.Details{ + "error": err, + }, + ) + + log.Println("composer: parallel_driver_ledger_create: done") +} diff --git a/test/antithesis/workload/bin/cmds/parallel_driver_transactions/main.go b/test/antithesis/workload/bin/cmds/parallel_driver_transactions/main.go new file mode 100644 index 0000000000..882948a37a --- /dev/null +++ b/test/antithesis/workload/bin/cmds/parallel_driver_transactions/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "log" + "math/big" + + "github.com/alitto/pond" + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" + "github.com/formancehq/ledger/test/antithesis/internal" +) + +func main() { + log.Println("composer: parallel_driver_transactions") + + ctx := context.Background() + client := internal.NewClient() + + ledger, err := internal.GetRandomLedger(ctx, client) + if err != nil { + ledger = "default" + log.Printf("error getting random ledger: %s", err) + } + + const count = 100 + + totalAmount := big.NewInt(0) + + pool := pond.New(10, 10e3) + + for i := 0; i < count; i++ { + amount := internal.RandomBigInt() + totalAmount = totalAmount.Add(totalAmount, amount) + pool.Submit(func() { + res, err := RunTx(ctx, client, Transaction(), ledger) + assert.Sometimes(err == nil, "transaction was committed successfully", internal.Details{ + "ledger": ledger, + }) + assert.Always(!internal.IsServerError(res.GetHTTPMeta()), "no internal server error when committing transaction", internal.Details{ + "ledger": ledger, + "error": err, + }) + }) + } + + pool.StopAndWait() + + log.Println("composer: parallel_driver_transactions: done") +} + +type Postings []components.V2Posting + +func RunTx( + ctx context.Context, + client *client.Formance, + postings Postings, + ledger string, +) (*operations.V2CreateTransactionResponse, error) { + res, err := client.Ledger.V2.CreateTransaction(ctx, operations.V2CreateTransactionRequest{ + Ledger: ledger, + V2PostTransaction: components.V2PostTransaction{ + Postings: postings, + }, + }) + + return res, err +} + +func Transaction() []components.V2Posting { + postings := []components.V2Posting{} + + postings = append(postings, components.V2Posting{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "orders:1234", + Source: "world", + }) + + return postings +} + +func Sequence() []Postings { + postings := []Postings{} + + for i := 0; i < 10; i++ { + postings = append(postings, Transaction()) + } + + return postings +} diff --git a/test/antithesis/workload/bin/init/main.go b/test/antithesis/workload/bin/init/main.go new file mode 100644 index 0000000000..e58198415d --- /dev/null +++ b/test/antithesis/workload/bin/init/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + "time" + + "github.com/antithesishq/antithesis-sdk-go/lifecycle" + "github.com/formancehq/ledger/test/antithesis/internal" +) + +func main() { + ctx := context.Background() + client := internal.NewClient() + + for { + time.Sleep(time.Second) + + _, err := client.Ledger.GetInfo(ctx) + if err != nil { + continue + } + break + } + + lifecycle.SetupComplete(map[string]any{}) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + <-sigs +} diff --git a/test/antithesis/workload/datagen/example.json b/test/antithesis/workload/datagen/example.json new file mode 100644 index 0000000000..ec2649bb49 --- /dev/null +++ b/test/antithesis/workload/datagen/example.json @@ -0,0 +1,3 @@ +[ + {} +] \ No newline at end of file diff --git a/test/antithesis/workload/datagen/main.go b/test/antithesis/workload/datagen/main.go new file mode 100644 index 0000000000..a25249ac83 --- /dev/null +++ b/test/antithesis/workload/datagen/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "math/rand" +) + +type Sequence struct { +} + +func Account() string { + return fmt.Sprintf("%d", rand.Intn(10e9)) +} + +func main() { + fmt.Println(Account()) +} diff --git a/test/antithesis/workload/go.mod b/test/antithesis/workload/go.mod new file mode 100644 index 0000000000..53f78d6112 --- /dev/null +++ b/test/antithesis/workload/go.mod @@ -0,0 +1,25 @@ +module github.com/formancehq/ledger/test/antithesis + +go 1.22.0 + +replace github.com/formancehq/ledger/pkg/client => ../../../pkg/client + +require ( + github.com/alitto/pond v1.8.3 + github.com/antithesishq/antithesis-sdk-go v0.4.2 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 + github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 + go.uber.org/atomic v1.10.0 + golang.org/x/sync v0.9.0 // indirect; indirect, for singleflight package +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test/antithesis/workload/go.sum b/test/antithesis/workload/go.sum new file mode 100644 index 0000000000..1c3c2cf9c7 --- /dev/null +++ b/test/antithesis/workload/go.sum @@ -0,0 +1,35 @@ +github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= +github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= +github.com/antithesishq/antithesis-sdk-go v0.4.2 h1:cYLNRnojCYp6rKoLKdK6M9UKi9EahFXBtF6WR1vc6V0= +github.com/antithesishq/antithesis-sdk-go v0.4.2/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +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/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo= +github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/antithesis/workload/internal/utils.go b/test/antithesis/workload/internal/utils.go new file mode 100644 index 0000000000..4bb87f8890 --- /dev/null +++ b/test/antithesis/workload/internal/utils.go @@ -0,0 +1,100 @@ +package internal + +import ( + "context" + "fmt" + "math/big" + "math/rand" + "net/http" + + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/antithesishq/antithesis-sdk-go/random" + "github.com/formancehq/go-libs/v2/time" + "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" + "github.com/formancehq/ledger/pkg/client/retry" +) + +type Details map[string]any + +func RandomBigInt() *big.Int { + v := random.GetRandom() + ret := big.NewInt(0) + ret.SetString(fmt.Sprintf("%d", v), 10) + return ret +} + +func AssertAlways(condition bool, message string, details map[string]any) bool { + assert.Always(condition, message, details) + return condition +} + +func AssertAlwaysErrNil(err error, message string, details map[string]any) bool { + return AssertAlways(err == nil, message, Details{ + "error": fmt.Sprint(err), + "details": details, + }) +} + +func NewClient() *client.Formance { + return client.New( + client.WithServerURL("http://gateway:8080"), + client.WithClient(&http.Client{ + Timeout: time.Minute, + }), + client.WithRetryConfig(retry.Config{ + Strategy: "backoff", + Backoff: &retry.BackoffStrategy{ + InitialInterval: 200, + Exponent: 1.5, + MaxElapsedTime: 10_000, + }, + RetryConnectionErrors: true, + }), + ) +} + +func IsServerError(httpMeta components.HTTPMetadata) bool { + return httpMeta.Response.StatusCode >= 400 && httpMeta.Response.StatusCode < 600 +} + +func CreateLedger(ctx context.Context, client *client.Formance, name string, bucket string) (*operations.V2CreateLedgerResponse, error) { + res, err := client.Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{ + Ledger: name, + V2CreateLedgerRequest: components.V2CreateLedgerRequest{ + Bucket: &bucket, + }, + }) + + return res, err +} + +func ListLedgers(ctx context.Context, client *client.Formance) ([]string, error) { + res, err := client.Ledger.V2.ListLedgers(ctx, operations.V2ListLedgersRequest{}) + if err != nil { + return nil, err + } + + ledgers := []string{} + for _, ledger := range res.V2LedgerListResponse.Cursor.Data { + ledgers = append(ledgers, ledger.Name) + } + + return ledgers, nil +} + +func GetRandomLedger(ctx context.Context, client *client.Formance) (string, error) { + ledgers, err := ListLedgers(ctx, client) + if err != nil { + return "", err + } + + if len(ledgers) == 0 { + return "", fmt.Errorf("no ledgers found") + } + + randomIndex := rand.Intn(len(ledgers)) + + return ledgers[randomIndex], nil +} diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index 3825417726..7f5e00d172 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -5,6 +5,10 @@ import ( "crypto/tls" "errors" "fmt" + "net/http" + "os" + "strings" + "github.com/formancehq/go-libs/v3/logging" ledgerclient "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" @@ -14,9 +18,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" - "net/http" - "os" - "strings" ) const (