Integrasi backend ke Service Provider Interface (SPI) broker bursa luar negeri menuntut toleransi kesalahan yang ketat. Lonjakan latensi jaringan lintas batas atau degradasi broker dapat memicu timeout cascading, penolakan order (order rejection), hingga inkonsistensi saldo pengguna. Implementasi deployment canary yang dipadukan dengan circuit breaker berbasis metrik latensi p99 dan rasio error menjadi lapisan proteksi utama untuk mencegah kerugian finansial sistemik.

Arsitektur Observabilitas Latency SPI Broker

Saat merutekan order ke broker eksternal, pemantauan latensi rata-rata (mean/average) menyembunyikan anomali eksekusi. Analisis harus berfokus pada persentil tinggi (p95 dan p99) untuk mendeteksi antrean jaringan (head-of-line blocking) atau throttling dari SPI broker. Metrik latensi diekspos menggunakan histogram Prometheus dengan bucket terukur.

package main

import (
	"context"
	"errors"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/sony/gobreaker"
)

var (
	spiLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "spi_execution_duration_seconds",
		Help:    "Distribusi latensi panggilan eksekusi SPI broker.",
		Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0},
	}, []string{"broker", "action"})

	orderRejections = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "spi_order_rejection_total",
		Help: "Jumlah order yang ditolak SPI broker.",
	}, []string{"broker", "reason_code"})
)

type BrokerClient struct {
	cb         *gobreaker.CircuitBreaker
	httpClient *http.Client
}

func NewBrokerClient() *BrokerClient {
	st := gobreaker.Settings{
		Name:        "US-Stock-SPI",
		MaxRequests: 5,
		Interval:    10 * time.Second,
		Timeout:     30 * time.Second,
		ReadyToTrip: func(counts gobreaker.Counts) bool {
			failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
			return counts.Requests >= 20 && failureRatio >= 0.20
		},
	}
	return &BrokerClient{
		cb:         gobreaker.NewCircuitBreaker(st),
		httpClient: &http.Client{Timeout: 3 * time.Second},
	}
}

func (c *BrokerClient) ExecuteOrder(ctx context.Context, orderPayload []byte) error {
	start := time.Now()
	_, err := c.cb.Execute(func() (interface{}, error) {
		req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://spi.broker.internal/v1/orders", nil)
		res, doErr := c.httpClient.Do(req)
		if doErr != nil {
			return nil, doErr
		}
		defer res.Body.Close()

		if res.StatusCode >= 500 || res.StatusCode == http.StatusTooManyRequests {
			orderRejections.WithLabelValues("interactive_brokers", "upstream_5xx").Inc()
			return nil, errors.New("upstream failure")
		}
		return nil, nil
	})
	spiLatency.WithLabelValues("interactive_brokers", "buy").Observe(time.Since(start).Seconds())
	return err
}

Prometheus Alert dan Automated Canary Rollback

Saat merilis patch integrasi trading (misalnya penyesuaian payload FIX atau REST protocol), deployment canary dibatasi ke 5-10% traffic. Sistem routing (seperti Flagger, Argo Rollouts, atau ingress controller) harus membaca metrik Prometheus dan otomatis membatalkan canary jika batas aman terlampaui.

groups:
  - name: stock-spi-canary.rules
    rules:
      - alert: CanarySPIp99LatencyHigh
        expr: |
          histogram_quantile(0.99, sum(rate(spi_execution_duration_seconds_bucket{canary="true"}[1m])) by (le)) > 1.5
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "Latensi Canary p99 SPI melebihi ambang batas toleransi 1.5s"

      - alert: CanaryOrderRejectionSpike
        expr: |
          sum(rate(spi_order_rejection_total{canary="true"}[1m]))
          /
          sum(rate(spi_execution_duration_seconds_count{canary="true"}[1m])) > 0.05
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "Rejection rate order pada instance canary melebihi 5%"

Webhook controller menangkap alert ini untuk segera mengubah bobot traffic canary ke 0% (traffic shifting reset) dan menjaga pod versi stable tetap melayani transaksi pengguna.

Mitigasi Split-State Saat Eksekusi Terputus

Masalah paling berbahaya pada integrasi bursa adalah timeout jaringan di sisi backend ketika order sesungguhnya telah diterima oleh mesin pencocokan bursa (matching engine). Circuit breaker yang terbuka secara lokal berisiko meninggalkan status split-state: backend mencatat order GAGAL, namun broker mengeksekusi PEMBELIAN, memicu pengurangan saldo ganda atau selisih margin.

  • Client-Assigned ClOrdID: Kirim Client Order ID unik (UUIDv7 atau Snowflake ID) di header request ke SPI. Broker wajib mengabaikan request duplikat jika ClOrdID yang sama dikirim ulang.
  • Toleransi Status IN_DOUBT: Jangan ubah status order menjadi FAILED saat circuit breaker trip akibat read timeout. Tandai transaksi sebagai PENDING_CONFIRMATION atau IN_DOUBT.
  • Distributed Outbox & Polling Recovery: Buat worker asinkron terpisah untuk memvalidasi status order ke SPI broker secara terjadwal menggunakan order query endpoint sebelum dana nasabah di-refund.

Postmortem: Penanganan Order Tertahan (In-Doubt Orders)

Jika degradasi SPI memicu circuit breaker terbuka secara masif dan sejumlah order tertahan, terapkan alur rekonsiliasi deterministik:

  1. Freeze Balance Sync: Kunci penarikan saldo dan trade lanjutan untuk akun yang memiliki status order IN_DOUBT.
  2. Broker Reconciliation Log: Unduh drop-copy atau trade logs akhir hari (EOD) dari broker SPI untuk mencocokkan status ClOrdID.
  3. Compensating Transaction: Jika broker memproses order tetapi sistem lokal mencatat timeout, buat entri jurnal manual (compensating transaction) untuk menyelaraskan kepemilikan lot saham dan saldo kas.
  4. Postmortem Root-Cause: Evaluasi apakah circuit breaker trip akibat network boundary (misal routing peering lintas negara) atau rate limit SPI. Sesuaikan Interval dan MaxRequests pada half-open state agar recovery berlangsung gradual.