Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e04092d
Add Scylla historical state offload prototype
Kbhat1 May 11, 2026
32b55d2
Avoid retry timer leak in Scylla consumer
Kbhat1 May 12, 2026
6c6f589
Parallelize Scylla mutation ingest
Kbhat1 May 12, 2026
4fcd242
Parallelize Scylla historical batch reads
Kbhat1 May 12, 2026
0fa1aef
Cache Scylla historical point reads
Kbhat1 May 13, 2026
5ef70ba
Enable token-aware Scylla routing
Kbhat1 May 13, 2026
37d0086
Compact duplicate Scylla mutation writes
Kbhat1 May 13, 2026
4a14131
Pipeline Scylla batch row writes
Kbhat1 May 13, 2026
f11f0e8
Add Bigtable historical offload backend
Kbhat1 May 14, 2026
d3035ec
Simplify Bigtable offload batching
Kbhat1 May 14, 2026
9890f78
Fix Bigtable lint findings
Kbhat1 May 14, 2026
66f8c9f
Remove unused historical BatchGet interface
Kbhat1 May 14, 2026
ecdf5cc
Reduce Bigtable write allocation overhead
Kbhat1 May 15, 2026
35f7143
Tune Bigtable consumer batch defaults
Kbhat1 May 15, 2026
429ee45
Batch Bigtable version marker writes
Kbhat1 May 18, 2026
a406321
Parallelize Bigtable latest version reads
Kbhat1 May 18, 2026
1a9736a
Optimize Bigtable historical reads
Kbhat1 May 26, 2026
571a943
Cache historical fallback misses
Kbhat1 May 27, 2026
b3e0b81
Simplify Bigtable offload helpers
Kbhat1 May 28, 2026
4e37dd6
Prepare Bigtable offload for review
Kbhat1 Jun 1, 2026
eea4a85
Merge branch 'main' into mvcc-bigtable
Kbhat1 Jun 1, 2026
c564e9e
Align historical fallback with main iterator type
Kbhat1 Jun 1, 2026
292f501
Refresh PR checks after labeling
Kbhat1 Jun 1, 2026
e2b358f
Parallelize Bigtable mutation batches by locality
Kbhat1 Jun 1, 2026
2435876
Fix Bigtable chunking tests under race detector
Kbhat1 Jun 1, 2026
3b0a3e6
Measure Bigtable read/write cost for historical state
Kbhat1 Jul 2, 2026
87a9d5f
Export consumer Prometheus metrics so Bigtable cost is scrapeable
Kbhat1 Jul 7, 2026
9a4a366
Add consumer throughput and Kafka lag metrics
Kbhat1 Jul 7, 2026
d40b29f
Harden historical fallback reads
Kbhat1 Jul 14, 2026
af6f3f9
Tighten offload consumer validation
Kbhat1 Jul 14, 2026
c3eac8d
Optimize backend readers and dedupe Scylla helpers
Kbhat1 Jul 14, 2026
024c3a8
Collapse redelivered versions in consumer batches
Kbhat1 Jul 16, 2026
ab50f39
Count fallback read outcomes
Kbhat1 Jul 16, 2026
8d4044f
Merge branch 'main' into mvcc-bigtable
Kbhat1 Jul 16, 2026
3a2d4d8
Gate fallback reads on declared and observed backend coverage
Kbhat1 Jul 17, 2026
d44d816
Support SASL/PLAIN for Google Cloud Managed Kafka
Kbhat1 Jul 17, 2026
284d1c1
Retry transient Bigtable read failures and keep connections alive
Kbhat1 Jul 17, 2026
9fc34be
Distinguish backend-behind from backend-error in fallback metrics
Kbhat1 Jul 17, 2026
2fa06ee
Trim dead knobs and indirection after style audit
Kbhat1 Jul 17, 2026
44c2b26
Fail loudly on garbage Scylla hosts config
Kbhat1 Jul 17, 2026
cbfb450
Remove the Scylla backend; the offload is Bigtable-only
Kbhat1 Jul 17, 2026
66b3d0e
Collapse the sink to a single WriteBatch interface
Kbhat1 Jul 17, 2026
db3f1bd
Merge remote-tracking branch 'origin/mvcc-bigtable-pt1' into mvcc-big…
Kbhat1 Jul 17, 2026
57a67ac
Merge branch 'mvcc-bigtable-pt1' into mvcc-bigtable
Kbhat1 Jul 21, 2026
8cb49c1
Gate historical offload behind an explicit backend config
Kbhat1 Jul 21, 2026
6cc315f
Merge branch 'mvcc-bigtable-pt1' into mvcc-bigtable
Kbhat1 Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/seidb.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ const (
FlagEVMSSDirectory = "state-store.evm-ss-db-directory"
FlagEVMSSSplit = "state-store.evm-ss-split"
FlagEVMSSSeparateDBs = "state-store.evm-ss-separate-dbs"

// Historical SS offload fallback.
FlagHistoricalOffloadBackend = "state-store.historical-offload-backend"
FlagHistoricalOffloadBigtableProjectID = "state-store.historical-offload-bigtable-project-id"
FlagHistoricalOffloadBigtableInstance = "state-store.historical-offload-bigtable-instance"
FlagHistoricalOffloadBigtableTable = "state-store.historical-offload-bigtable-table"
FlagHistoricalOffloadBigtableFamily = "state-store.historical-offload-bigtable-family"
FlagHistoricalOffloadBigtableAppProfile = "state-store.historical-offload-bigtable-app-profile"
FlagHistoricalOffloadBigtableShards = "state-store.historical-offload-bigtable-shards"
FlagHistoricalOffloadEarliestVersion = "state-store.historical-offload-earliest-version"
)

var GigaKeys = []string{"evm", "bank"}
Expand Down Expand Up @@ -208,5 +218,13 @@ func parseSSConfigs(appOpts servertypes.AppOptions) config.StateStoreConfig {
ssConfig.EVMDBDirectory = cast.ToString(appOpts.Get(FlagEVMSSDirectory))
ssConfig.SeparateEVMSubDBs = cast.ToBool(appOpts.Get(FlagEVMSSSeparateDBs))
ssConfig.EVMSplit = cast.ToBool(appOpts.Get(FlagEVMSSSplit))
ssConfig.HistoricalOffloadBackend = cast.ToString(appOpts.Get(FlagHistoricalOffloadBackend))
ssConfig.HistoricalOffloadBigtableProjectID = cast.ToString(appOpts.Get(FlagHistoricalOffloadBigtableProjectID))
ssConfig.HistoricalOffloadBigtableInstance = cast.ToString(appOpts.Get(FlagHistoricalOffloadBigtableInstance))
ssConfig.HistoricalOffloadBigtableTable = cast.ToString(appOpts.Get(FlagHistoricalOffloadBigtableTable))
ssConfig.HistoricalOffloadBigtableFamily = cast.ToString(appOpts.Get(FlagHistoricalOffloadBigtableFamily))
ssConfig.HistoricalOffloadBigtableAppProfile = cast.ToString(appOpts.Get(FlagHistoricalOffloadBigtableAppProfile))
ssConfig.HistoricalOffloadBigtableShards = cast.ToInt(appOpts.Get(FlagHistoricalOffloadBigtableShards))
ssConfig.HistoricalOffloadEarliestVersion = cast.ToInt64(appOpts.Get(FlagHistoricalOffloadEarliestVersion))
return ssConfig
}
42 changes: 42 additions & 0 deletions app/seidb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ func (t TestSeiDBAppOpts) Get(s string) interface{} {
return defaultSSConfig.EVMSplit
case FlagEVMSSSeparateDBs:
return defaultSSConfig.SeparateEVMSubDBs
case FlagHistoricalOffloadBackend:
return defaultSSConfig.HistoricalOffloadBackend
case FlagHistoricalOffloadBigtableProjectID:
return defaultSSConfig.HistoricalOffloadBigtableProjectID
case FlagHistoricalOffloadBigtableInstance:
return defaultSSConfig.HistoricalOffloadBigtableInstance
case FlagHistoricalOffloadBigtableTable:
return defaultSSConfig.HistoricalOffloadBigtableTable
case FlagHistoricalOffloadBigtableFamily:
return defaultSSConfig.HistoricalOffloadBigtableFamily
case FlagHistoricalOffloadBigtableAppProfile:
return defaultSSConfig.HistoricalOffloadBigtableAppProfile
case FlagHistoricalOffloadBigtableShards:
return defaultSSConfig.HistoricalOffloadBigtableShards
case FlagHistoricalOffloadEarliestVersion:
return defaultSSConfig.HistoricalOffloadEarliestVersion
}
return nil
}
Expand Down Expand Up @@ -204,6 +220,32 @@ func TestParseSSConfigs_EVMFlags(t *testing.T) {
assert.True(t, ssConfig.SeparateEVMSubDBs)
}

func TestParseSSConfigs_HistoricalBigtableFlags(t *testing.T) {
appOpts := mapAppOpts{
FlagSSEnable: true,
FlagHistoricalOffloadBackend: "bigtable",
FlagHistoricalOffloadBigtableProjectID: "sei-project",
FlagHistoricalOffloadBigtableInstance: "sei-history",
FlagHistoricalOffloadBigtableTable: "state_mutations",
FlagHistoricalOffloadBigtableFamily: "state",
FlagHistoricalOffloadBigtableAppProfile: "historical",
FlagHistoricalOffloadBigtableShards: 512,
FlagHistoricalOffloadEarliestVersion: int64(1234),
FlagSSAsyncWriterBuffer: 0,
}

ssConfig := parseSSConfigs(appOpts)
assert.True(t, ssConfig.Enable)
assert.Equal(t, "bigtable", ssConfig.HistoricalOffloadBackend)
assert.Equal(t, "sei-project", ssConfig.HistoricalOffloadBigtableProjectID)
assert.Equal(t, "sei-history", ssConfig.HistoricalOffloadBigtableInstance)
assert.Equal(t, "state_mutations", ssConfig.HistoricalOffloadBigtableTable)
assert.Equal(t, "state", ssConfig.HistoricalOffloadBigtableFamily)
assert.Equal(t, "historical", ssConfig.HistoricalOffloadBigtableAppProfile)
assert.Equal(t, 512, ssConfig.HistoricalOffloadBigtableShards)
assert.Equal(t, int64(1234), ssConfig.HistoricalOffloadEarliestVersion)
}

func TestParseSSConfigs_ReadWriteMetrics(t *testing.T) {
ssConfig := parseSSConfigs(mapAppOpts{
FlagSSEnable: true,
Expand Down
20 changes: 20 additions & 0 deletions sei-db/config/ss_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ type StateStoreConfig struct {
// When true, data is routed to separate DBs by EVM key family while
// preserving the same logical store key and full key encoding inside each DB.
SeparateEVMSubDBs bool `mapstructure:"evm-separate-dbs"`

// HistoricalOffloadBackend selects the historical offload read fallback.
// Empty (the default) disables the fallback entirely; "bigtable" enables
// the Bigtable reader configured by the connection fields below.
HistoricalOffloadBackend string `mapstructure:"historical-offload-backend"`

// HistoricalOffloadBigtable* are the Bigtable connection parameters used
// when HistoricalOffloadBackend is "bigtable".
HistoricalOffloadBigtableProjectID string `mapstructure:"historical-offload-bigtable-project-id"`
HistoricalOffloadBigtableInstance string `mapstructure:"historical-offload-bigtable-instance"`
HistoricalOffloadBigtableTable string `mapstructure:"historical-offload-bigtable-table"`
HistoricalOffloadBigtableFamily string `mapstructure:"historical-offload-bigtable-family"`
HistoricalOffloadBigtableAppProfile string `mapstructure:"historical-offload-bigtable-app-profile"`
HistoricalOffloadBigtableShards int `mapstructure:"historical-offload-bigtable-shards"`
Comment on lines +91 to +96

@yzang2019 yzang2019 Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion:

  1. Move these configs to one struct
  2. Add a FallbackSSBackend = "BigTable" config, and default to empty (meaning disabled)


// HistoricalOffloadEarliestVersion is the operator-declared earliest version
// (inclusive) fully ingested into the historical backend. When > 0 it is
// advertised as the store's earliest version so height gates admit pruned
// reads; reads below it stay on the primary.
HistoricalOffloadEarliestVersion int64 `mapstructure:"historical-offload-earliest-version"`
}

// DefaultStateStoreConfig returns the default StateStoreConfig
Expand Down
4 changes: 4 additions & 0 deletions sei-db/config/toml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func TestStateStoreConfigTemplate(t *testing.T) {
require.Contains(t, output, `evm-ss-db-directory = ""`, "Missing evm-ss-db-directory")
require.Contains(t, output, `evm-ss-split = false`, "Missing or incorrect evm-ss-split")
require.Contains(t, output, "evm-ss-separate-dbs = false", "Missing or incorrect evm-ss-separate-dbs")
// The historical-offload keys are deliberately absent from the generated
// default config (internal/Giga-only for now); they still parse when an
// operator adds them to app.toml manually.
require.NotContains(t, output, "historical-offload", "historical-offload keys must not be in the default template")
}

// TestReceiptStoreConfigTemplate verifies that all field paths in the receipt-store TOML template
Expand Down
11 changes: 11 additions & 0 deletions sei-db/state_db/ss/composite/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@
return s.cosmosStore.GetEarliestVersion()
}

// GetEarliestVersionForKey reports the earliest version still held locally by
// the store that serves storeKey. Cosmos and EVM prune independently (an EVM
// prune failure is only logged), so pruned-read fallback must consult the
// horizon of the store that will actually serve the read.
func (s *CompositeStateStore) GetEarliestVersionForKey(storeKey string) int64 {
if s.evmRouted(storeKey) {
return s.evmStore.GetEarliestVersion()
}
return s.cosmosStore.GetEarliestVersion()
}

Check failure on line 231 in sei-db/state_db/ss/composite/store.go

View check run for this annotation

Claude / Claude Code Review

GetEarliestVersionForKey trusts EVMStateStore's MIN-across-sub-DBs earliest, defeating the new per-key fallback safety check

GetEarliestVersionForKey("evm") delegates to EVMStateStore.GetEarliestVersion(), which under evm-ss-separate-dbs takes the MINIMUM earliest version across the 5 independently-pruned EVM sub-DBs — the least-pruned (most optimistic) value, not a safe lower bound. If one sub-DB lags behind the others (e.g. a transient prune failure, since EVMStateStore.Prune only logs errors and never retries), FallbackStateStore.shouldFallback will see the lagging sub-DB's earliest and skip the Bigtable fallback f
Comment on lines +222 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 GetEarliestVersionForKey("evm") delegates to EVMStateStore.GetEarliestVersion(), which under evm-ss-separate-dbs takes the MINIMUM earliest version across the 5 independently-pruned EVM sub-DBs — the least-pruned (most optimistic) value, not a safe lower bound. If one sub-DB lags behind the others (e.g. a transient prune failure, since EVMStateStore.Prune only logs errors and never retries), FallbackStateStore.shouldFallback will see the lagging sub-DB's earliest and skip the Bigtable fallback for a read that actually needs to be routed to a different, more-pruned sub-DB — that sub-DB's own MVCC self-gate then silently returns (nil, nil) even though the data exists in Bigtable.

Extended reasoning...

The bug. GetEarliestVersionForKey(storeKey) in sei-db/state_db/ss/composite/store.go:222-231 is new in this PR and exists specifically so FallbackStateStore.shouldFallback (sei-db/state_db/ss/offload/historical/store.go) can consult 'the horizon of the store that will actually serve the read' rather than a single cosmos-only aggregate, per its own doc comment. For storeKey == "evm" it returns s.evmStore.GetEarliestVersion() verbatim. That pre-existing method (sei-db/state_db/ss/evm/store.go:154-165) computes the minimum earliest version across all managedDBs:

func (s *EVMStateStore) GetEarliestVersion() int64 {
    var minVersion int64 = -1
    for _, db := range s.managedDBs {
        if v := db.GetEarliestVersion(); minVersion < 0 || v < minVersion {
            minVersion = v
        }
    }
    ...
}

Why MIN is wrong here. When evm-ss-separate-dbs is enabled, EVMStateStore holds 5 independently-pruned sub-DBs (nonce/storage/code/codeHash/balance), and Get/Has route each key to exactly one specific sub-DB via routeKey. Each sub-DB's own MVCC layer self-gates reads: if targetVersion < db.GetEarliestVersion() { return nil, nil } (pebbledb/mvcc/db.go:401-403). A safe fallback-routing watermark must be a value V such that 'version < V' implies 'every sub-DB that could serve this key has already pruned past version' — i.e. the maximum earliest across sub-DBs, not the minimum. Taking the minimum answers a different, more optimistic question ('has any sub-DB pruned this far'), which is not what shouldFallback needs.

The failure path. EVMStateStore.Prune fans out pruning to all sub-DBs concurrently via a WaitGroup and returns only the first error — every sub-DB's own prune goroutine runs to completion regardless of a sibling's failure. CompositeStateStore.Prune only logs an evmStore.Prune error, it never propagates or retries it. So a single transient failure (e.g. a disk I/O hiccup) in one sub-DB during one prune cycle leaves it behind while its siblings successfully advance. Concretely: code sub-DB fails to prune and stays at earliest=500, while storage successfully advances to earliest=1000. GetEarliestVersion() now reports MIN(500, 1000, ...) = 500.

Step-by-step proof. (1) A read arrives for a storage-family EVM key at version 700, with a Bigtable backend configured and covering this range. (2) FallbackStateStore.shouldFallback("evm", 700) calls earliestVersionFor("evm")GetEarliestVersionForKey("evm")evmStore.GetEarliestVersion() = 500. (3) Since 700 < 500 is false, shouldFallback returns false, so the read is not routed to Bigtable and instead goes to the local primary. (4) EVMStateStore.Get routes the key to the storage sub-DB (earliest=1000) via routeKey. (5) That sub-DB's MVCC self-gate checks 700 < 1000 → true → returns (nil, nil), a silent empty result, even though the row exists in Bigtable and would have been served correctly had shouldFallback returned true.

Why nothing catches this today. Before this PR, EVMStateStore.GetEarliestVersion()'s MIN-vs-MAX choice was dormant: CompositeStateStore.GetEarliestVersion() (the only prior consumer, used by the RPC height gate) exclusively reads cosmosStore.GetEarliestVersion() and never touches the EVM aggregate. This PR's GetEarliestVersionForKey is the first caller that uses the EVM aggregate as a read-routing safety decision, so a previously-inert aggregation quirk becomes a genuine, if narrow, silent-data-loss path — precisely the kind of per-store divergence PerKeyEarliestVersioner was introduced to catch, just one level too shallow (it stops at the whole EVMStateStore, not the individual sub-DB that will actually serve the key).

Fix. Either change EVMStateStore.GetEarliestVersion() to take the maximum across managedDBs when used as a safe-to-read watermark (over-eager fallback is harmless since Bigtable rows are immutable), or have GetEarliestVersionForKey route through the specific sub-DB that will actually serve storeKey+key (not achievable today since FallbackStateStore only has the store key, not the specific key, so MAX is the more directly actionable fix).

Scope/severity note. This requires stacking two non-default, internal/Giga-only features (evm-ss-separate-dbs and the historical-offload fallback) plus a transient partial-prune failure creating sub-DB divergence, and it self-heals on the sub-DB's next successful prune cycle. Within that window, though, it is a genuine silent-empty read for data that exists in the historical backend, served over the exact RPC path (EVM height-gated reads) this PR is trying to make correct.


func (s *CompositeStateStore) Close() error {
s.closeOnce.Do(func() {
if s.pruningManager != nil {
Expand Down
49 changes: 47 additions & 2 deletions sei-db/state_db/ss/offload/consumer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,55 @@ SASL/PLAIN using service-account credentials:
}
```

## Node Read Fallback

Enable fallback reads in the node config:

```toml
[state-store]
historical-offload-backend = "bigtable"
historical-offload-bigtable-project-id = "my-gcp-project"
historical-offload-bigtable-instance = "sei-history"
historical-offload-bigtable-table = "state_mutations"
historical-offload-bigtable-family = "state"
historical-offload-bigtable-app-profile = ""
historical-offload-bigtable-shards = 256
```

These keys are deliberately not part of the generated default config
(internal/Giga-only for now) and must be added to `app.toml` manually.

Fallback activates only for point reads where the requested version is below the
local SS earliest version. Missing rows and tombstones return empty state, same
as local SS. Reads ahead of the backend's last ingested version (consumer lag)
return an error rather than empty state.

To open RPC height gates for pruned heights, declare how far back the backend
has full coverage (the version at which ingestion/backfill started):

```toml
[state-store]
historical-offload-earliest-version = 123456789
```

When set (> 0), the node advertises this as its earliest version so height
checks admit heights the fallback can serve; point reads below it stay on the
local store. Leave it 0 until the backend actually covers the target range —
heights the backend never ingested would otherwise read as empty state.

## Operational preconditions

Before enabling the node read fallback in production:

- The Bigtable table exists with the same family/shards the consumer wrote with.
- The consumer has been ingesting continuously; check
`consumer_kafka_lag` / `bigtable_rows_mutated_total`.
- `historical-offload-earliest-version` is set to the true coverage floor.

## Current Limits

- The node-side read fallback lands in part 2; this part is the client library
and the ingestion pipeline.
- No offload iterator path; range queries between the coverage floor and the
local prune horizon see only local data.
- No cross-row transaction on ingest; mutation rows are written first and the
version marker is written last, so replay is idempotent after partial failure.
- No automatic table creation from the binary.
Expand Down
49 changes: 49 additions & 0 deletions sei-db/state_db/ss/offload/historical/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package historical

import (
"context"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

// fallbackMeterName is the OpenTelemetry meter for the node-side pruned-read
// fallback path, exported through the same process-wide MeterProvider.
const fallbackMeterName = "seidb_historical_fallback"

const (
fallbackOutcomeCacheHit = "cache_hit"
fallbackOutcomeBackendHit = "backend_hit"
fallbackOutcomeBackendMiss = "backend_miss"
fallbackOutcomeBackendBehind = "backend_behind"
fallbackOutcomeError = "error"
)

// fallbackMetrics counts pruned point reads by operation (get/has) and outcome
// so operators can see how much load the read cache absorbs and how many reads
// actually reach the historical backend. Attributes are a closed set, so
// cardinality stays flat.
type fallbackMetrics struct {
reads metric.Int64Counter
}

func newFallbackMetrics() *fallbackMetrics {
meter := otel.Meter(fallbackMeterName)
reads, _ := meter.Int64Counter(
"historical_fallback_reads_total",
metric.WithDescription("Pruned point reads routed to the historical fallback, by operation and outcome"),
metric.WithUnit("{read}"),
)
return &fallbackMetrics{reads: reads}
}

func (m *fallbackMetrics) recordRead(op, outcome string) {
if m == nil {
return
}
m.reads.Add(context.Background(), 1, metric.WithAttributes(
attribute.String("op", op),
attribute.String("outcome", outcome),
))
}
21 changes: 21 additions & 0 deletions sei-db/state_db/ss/offload/historical/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package historical

import "testing"

func TestFallbackMetricsNilSafe(t *testing.T) {
var m *fallbackMetrics
m.recordRead("get", fallbackOutcomeCacheHit)
}

func TestFallbackMetricsRecordNoPanic(t *testing.T) {
m := newFallbackMetrics()
for _, outcome := range []string{
fallbackOutcomeCacheHit,
fallbackOutcomeBackendHit,
fallbackOutcomeBackendMiss,
fallbackOutcomeError,
} {
m.recordRead("get", outcome)
m.recordRead("has", outcome)
}
}
Loading
Loading