-
Notifications
You must be signed in to change notification settings - Fork 886
Bound concurrent CheckTx on the producer ingest path #4182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
991339c
Bound concurrent CheckTx on the producer ingest path
masih 9ba141a
Merge giga-1 into bounded CheckTx concurrency
masih fe96aca
Address review: detach async insert ctx, Option config, permit wait m…
masih ea4a03e
Merge origin/giga-1 into masih/1789479448-bound-checktx-concurrency
masih 8b53310
Merge branch 'giga-1' into masih/1789479448-bound-checktx-concurrency
masih File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
sei-tendermint/internal/autobahn/producer/checktx_limit_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| package producer | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "runtime" | ||
| "testing" | ||
|
|
||
| abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" | ||
| "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" | ||
| ) | ||
|
|
||
| type gatedCheckTx struct { | ||
| open bool | ||
| inflight int | ||
| maxSeen int | ||
| } | ||
|
|
||
| // gatedApp is a testApp whose CheckTx blocks until the gate is opened, | ||
| // tracking how many CheckTx calls are in flight at once. | ||
| type gatedApp struct { | ||
| *testApp | ||
| gate utils.Watch[*gatedCheckTx] | ||
| } | ||
|
|
||
| func newGatedApp() *gatedApp { | ||
| return &gatedApp{ | ||
| testApp: newTestApp(), | ||
| gate: utils.NewWatch(&gatedCheckTx{}), | ||
| } | ||
| } | ||
|
|
||
| func (a *gatedApp) Proxy() *proxy.Proxy { | ||
| return proxy.New(a) | ||
| } | ||
|
|
||
| func (a *gatedApp) CheckTx(ctx context.Context, req *abci.RequestCheckTxV2) *abci.ResponseCheckTxV2 { | ||
| for g, ctrl := range a.gate.Lock() { | ||
| g.inflight += 1 | ||
| g.maxSeen = max(g.maxSeen, g.inflight) | ||
| ctrl.Updated() | ||
| err := ctrl.WaitUntil(ctx, func() bool { return g.open }) | ||
| g.inflight -= 1 | ||
| ctrl.Updated() | ||
| if err != nil { | ||
| return &abci.ResponseCheckTxV2{ResponseCheckTx: &abci.ResponseCheckTx{Code: 1, Log: err.Error()}} | ||
| } | ||
| } | ||
| return a.testApp.CheckTx(ctx, req) | ||
| } | ||
|
|
||
| // waitInflight blocks until exactly n CheckTx calls are in flight. | ||
| func (a *gatedApp) waitInflight(ctx context.Context, n int) error { | ||
| for g, ctrl := range a.gate.Lock() { | ||
| return ctrl.WaitUntil(ctx, func() bool { return g.inflight == n }) | ||
| } | ||
| panic("unreachable") | ||
| } | ||
|
|
||
| func (a *gatedApp) openGate() { | ||
| for g, ctrl := range a.gate.Lock() { | ||
| g.open = true | ||
| ctrl.Updated() | ||
| } | ||
| } | ||
|
|
||
| func (a *gatedApp) maxInflight() int { | ||
| for g := range a.gate.Lock() { | ||
| return g.maxSeen | ||
| } | ||
| panic("unreachable") | ||
| } | ||
|
|
||
| // With limit N and M>N concurrent inserts, at most N CheckTx calls run at once | ||
| // and every tx is still admitted. | ||
| func TestInsertTx_BoundsConcurrentCheckTx(t *testing.T) { | ||
| for _, limit := range utils.Slice(1, 2, 3) { | ||
| t.Run(fmt.Sprint(limit), func(t *testing.T) { | ||
| ctx := t.Context() | ||
| rng := utils.TestRng() | ||
| app := newGatedApp() | ||
| cfg := app.Cfg() | ||
| cfg.MaxConcurrentCheckTx = utils.Some(uint64(limit)) //nolint:gosec // small test constant | ||
| env := newTestEnv(rng, cfg, app.Proxy()) | ||
| env.alignLocalMempool() | ||
|
|
||
| const m = 6 | ||
| txs := make([][]byte, 0, m) | ||
| for range m { | ||
| addr, nonce := app.NewAccount(rng) | ||
| txs = append(txs, env.genTx(rng, addr, nonce).encode()) | ||
| } | ||
| require.NoError(t, scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { | ||
| for _, tx := range txs { | ||
| s.Spawn(func() error { | ||
| _, err := env.state.InsertTx(ctx, tx) | ||
| return err | ||
| }) | ||
| } | ||
| if err := app.waitInflight(ctx, limit); err != nil { | ||
| return err | ||
| } | ||
| app.openGate() | ||
| return nil | ||
| })) | ||
| require.Equal(t, limit, app.maxInflight()) | ||
| require.Equal(t, m, len(env.state.UnconfirmedTxs())) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Waiters cancelled while queued for a CheckTx permit return ctx error and | ||
| // do not consume a permit, so later inserts still get through. | ||
| func TestInsertTx_CancelledCheckTxWaiterReleasesPermit(t *testing.T) { | ||
| ctx := t.Context() | ||
| rng := utils.TestRng() | ||
| app := newGatedApp() | ||
| cfg := app.Cfg() | ||
| cfg.MaxConcurrentCheckTx = utils.Some[uint64](1) | ||
| env := newTestEnv(rng, cfg, app.Proxy()) | ||
| env.alignLocalMempool() | ||
|
|
||
| newTx := func() []byte { | ||
| addr, nonce := app.NewAccount(rng) | ||
| return env.genTx(rng, addr, nonce).encode() | ||
| } | ||
| require.NoError(t, scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { | ||
| holder := newTx() | ||
| s.Spawn(func() error { | ||
| _, err := env.state.InsertTx(ctx, holder) | ||
| return err | ||
| }) | ||
| // The single permit is now held by holder, blocked in CheckTx. | ||
| if err := app.waitInflight(ctx, 1); err != nil { | ||
| return err | ||
| } | ||
| waitCtx, cancel := context.WithCancel(ctx) | ||
| if err := scope.Run(waitCtx, func(ctx context.Context, s scope.Scope) error { | ||
| for range 4 { | ||
| tx := newTx() | ||
| s.Spawn(func() error { | ||
| _, err := env.state.InsertTx(ctx, tx) | ||
| if !errors.Is(err, context.Canceled) { | ||
| return fmt.Errorf("InsertTx() = %v, want context.Canceled", err) | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
| cancel() | ||
| return nil | ||
| }); err != nil { | ||
| return err | ||
| } | ||
| app.openGate() | ||
| // Once holder finishes, a fresh insert must acquire the permit. | ||
| _, err := env.state.InsertTx(ctx, newTx()) | ||
| return err | ||
| })) | ||
| require.Equal(t, 1, app.maxInflight()) | ||
| require.Equal(t, 2, len(env.state.UnconfirmedTxs())) | ||
| } | ||
|
|
||
| // An absent limit resolves to at least one permit so inserts proceed. | ||
| func TestConfig_MaxConcurrentCheckTxDefault(t *testing.T) { | ||
| cfg := &Config{} | ||
| require.True(t, cfg.maxConcurrentCheckTx() >= 1) | ||
| require.Equal(t, max(1, runtime.GOMAXPROCS(0)/2), cfg.maxConcurrentCheckTx()) | ||
| cfg.MaxConcurrentCheckTx = utils.Some[uint64](7) | ||
| require.Equal(t, 7, cfg.maxConcurrentCheckTx()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 13 additions & 1 deletion
14
sei-tendermint/internal/autobahn/producer/metrics/metrics.gen.go
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion] The permit wait turns
TryInsertTxfrom fail-fast into an unbounded, now-unsheddable queue.TryInsertTxis documented as "returns error if mempool is full" andBroadcastTxAsyncrelies on that to be best-effort and non-blocking; with the permit acquired before the mempool state is consulted, every async broadcast parks here first, and since that goroutine is now handedcontext.WithoutCancel(ctx)nothing sheds it — not a client disconnect, not node shutdown.Concretely: once arrival rate exceeds
MaxConcurrentCheckTx / checkTxLatency(the cap halves the previous service rate, so this threshold is now lower), waiters accumulate one goroutine plus the tx bytes per broadcast with no bound, and the RPC has already returned 200. The CPU starvation this PR fixes is real, but the trade is currently unbounded memory growth instead, with noinserts{result=...}signal distinguishing it — onlyin_flight{phase="check_tx_wait"}drifting upward.Bounding the queue on the non-waiting path would keep the fail-fast contract: e.g. a
TryAcquire(or a max-waiters count) that returnserrMempoolFullforwaitIfFull == false, so overload surfaces asinserts{result="full"}and the goroutine exits instead of parking indefinitely.InsertTx(syncbroadcast_tx/eth_sendRawTransaction) can keep blocking, since there the caller's ctx still provides backpressure.