feat: add chip-ingress batch config keys and tune defaults#2207
Conversation
✅ API Diff Results -
|
78f5646 to
1f54ac6
Compare
7c7d08a to
504389d
Compare
📊 API Diff Results
|
9428215 to
caf6101
Compare
There was a problem hiding this comment.
Pull request overview
Adds configurable chip-ingress batch emitter knobs end-to-end (beholder config ⇄ LOOP env config ⇄ server wiring), updates batch-emitter defaults based on capacity analysis, and corrects the chip_ingress.batch.request_size_* histogram bucket schemas.
Changes:
- Extend
beholder.ConfigwithChipIngressMaxGRPCRequestSizeand update chip-ingress batch default values. - Add
CL_CHIP_INGRESS_*env vars to LOOPEnvConfig, serialize/parse them, and wire them intobeholder.Configin the LOOP server. - Adjust chip-ingress batch client histogram bucket boundaries for request sizes (messages + bytes).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/beholder/config.go | Adds ChipIngressMaxGRPCRequestSize and updates chip-ingress batch emitter defaults. |
| pkg/beholder/batch_emitter_service.go | Wires max gRPC request size into the batch client configuration. |
| pkg/beholder/batch_emitter_service_test.go | Extends tests/bench config to include ChipIngressMaxGRPCRequestSize. |
| pkg/beholder/testdata/config-example.json | Adds the new config key to the example JSON schema. |
| pkg/chipingress/batch/client.go | Updates OTel histogram bucket boundaries for request size metrics. |
| pkg/loop/config.go | Adds new CL_CHIP_INGRESS_* env vars, EnvConfig fields, serialization, and parsing helpers. |
| pkg/loop/config_test.go | Updates env config tests to cover new chip-ingress env vars. |
| pkg/loop/server.go | Wires new LOOP env config fields into beholder.Config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| maxGRPCRequestSize := int(cfg.ChipIngressMaxGRPCRequestSize) | ||
| if maxGRPCRequestSize == 0 { | ||
| maxGRPCRequestSize = int(defaults.ChipIngressMaxGRPCRequestSize) | ||
| } |
Bump github.com/smartcontractkit/chainlink-common/pkg/chipingress in the main chainlink-common go.mod to the squashed chip-ingress-batch-config commit (caf6101) so the main module resolves the updated chipingress package that contains the chip_ingress.batch.request_size_* histogram bucket changes. This is a follow-up to #2207 and must merge after it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
pkg/loop/config.go:744
- getUint parses into a uint64 and then casts to uint without bounds checking. On 32-bit platforms (or if a very large value is provided), this can overflow and wrap silently, leading to incorrect configuration values.
func getUint(envKey string) (uint, error) {
s := os.Getenv(envKey)
if s == "" {
return 0, nil
}
u, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
return uint(u), nil
}
pkg/beholder/batch_emitter_service.go:70
- ChipIngressMaxGRPCRequestSize is converted from uint to int without range checks. If a user configures a value larger than max int, the cast can overflow and become negative/incorrect, which would be passed into batch.WithMaxGRPCRequestSize.
maxGRPCRequestSize := int(cfg.ChipIngressMaxGRPCRequestSize)
if maxGRPCRequestSize == 0 {
maxGRPCRequestSize = int(defaults.ChipIngressMaxGRPCRequestSize)
}
| type ChipIngressBatchEmitterService struct { | ||
| services.Service | ||
| eng *services.Engine | ||
|
|
||
| batchClient *batch.Client | ||
| batchClient *batch.Client | ||
| resourceAttrs map[string]string | ||
|
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
pkg/loop/config.go:613
ChipIngressMaxConcurrentSendsis parsed as anintbut negative values are currently accepted. A negative value will panic at runtime when passed intobatch.WithMaxConcurrentSends(it usesmake(chan struct{}, maxConcurrentSends)). Please reject negative values during env parsing (consistent with the existing negative checks for other chip-ingress keys).
e.ChipIngressMaxConcurrentSends, err = getInt(envChipIngressMaxConcurrentSends)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envChipIngressMaxConcurrentSends, err)
}
e.ChipIngressSendInterval, err = getDuration(envChipIngressSendInterval)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envChipIngressSendInterval, err)
}
pkg/loop/config.go:747
getUintparses into auint64and then casts touintwithout an overflow check. On 32-bit builds this can silently wrap, and even on 64-bit it can allow values that later overflow when converted toint(e.g., in beholder batch emitter config). Prefer parsing with a bitSize that matches the target word size so overflow becomes a parse error.
func getUint(envKey string) (uint, error) {
s := os.Getenv(envKey)
if s == "" {
return 0, nil
}
u, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
return uint(u), nil
}
0a4817d to
733491d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
pkg/loop/config.go:746
getUintparses into a uint64 and then casts touintwithout overflow checking. On 32-bit builds this can silently wrap, producing a much smaller value than configured. Add an explicit bounds check before casting.
u, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
return uint(u), nil
| // Add custom resource attributes | ||
| resource, err = sdkresource.Merge( | ||
| resource, | ||
| sdkresource.NewSchemaless(cfg.ResourceAttributes...), | ||
| resource, | ||
| ) |
| batch.WithMaxConcurrentSends(maxConcurrentSends), | ||
| batch.WithMaxGRPCRequestSize(maxGRPCRequestSize), | ||
| batch.WithEventClone(false), | ||
| batch.WithClientName(batch.ClientNameBeholder), | ||
| ) |
| chipingressbatch.WithMessageBuffer(defaultInt(cfg.MessageBufferSize, 10_000)), | ||
| chipingressbatch.WithMaxPublishTimeout(defaultDuration(cfg.MaxPublishTimeout, 5*time.Second)), | ||
| chipingressbatch.WithShutdownTimeout(defaultDuration(cfg.ShutdownTimeout, 30*time.Second)), | ||
| chipingressbatch.WithClientName(chipingressbatch.ClientNameDurableEmitter), | ||
| ) |
| if err := ms.Start(ctx, sub); err != nil { | ||
| s.eng.Errorw("Failed to start sub-service", "name", sub.Name(), "error", err) | ||
| return fmt.Errorf("failed to start sub-service %s of %s: %w", sub.Name(), s.cfg.Name, err) | ||
| s.eng.Errorw("Failed to start sub-service", "error", err) | ||
| return fmt.Errorf("failed to start sub-service of %s: %w", s.cfg.Name, err) | ||
| } |
| const ( | ||
| beholderDomain = "node-platform" | ||
| beholderProtobufEntity = "common.v1.HealthInfo" | ||
| beholderProtobufEntity = "common.v1.NodeHealthInfo" | ||
| beholderProtobufDataSchema = "/node-platform/common/v1" | ||
| ) |
| github.com/andybalholm/brotli v1.1.1 | ||
| github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c | ||
| github.com/bytecodealliance/wasmtime-go/v47 v47.0.0 | ||
| github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 | ||
| github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 |
| github.com/shopspring/decimal v1.4.0 | ||
| github.com/smartcontractkit/chain-selectors v1.0.100 | ||
| github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260716165322-7f2edff6e954 | ||
| github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62 | ||
| github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 |
Expose 7 chip-ingress batch emitter configuration keys in beholder.Config and wire them through the batch emitter and LOOP plugin env config. Default changes: - ChipIngressBufferSize: 1000 -> 10000 - ChipIngressMaxBatchSize: 500 -> 1000 - ChipIngressSendInterval: 100ms -> 500ms - ChipIngressSendTimeout: 3s -> 10s - ChipIngressDrainTimeout: 10s -> 30s Also fix the chip_ingress.batch.request_size_* histogram buckets to use explicit low-cardinality boundaries, and remove the previous Co-authored-by Cursor attribution from the commit history.
733491d to
d44059c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
pkg/loop/config.go:609
CL_CHIP_INGRESS_MAX_CONCURRENT_SENDSis parsed withgetIntbut isn't validated for negative values. A negative value will panic at runtime whenbatch.WithMaxConcurrentSendsdoesmake(chan struct{}, maxConcurrentSends)with a negative capacity. Add a non-negative validation similar toChipIngressMaxGRPCRequestSize.
e.ChipIngressMaxConcurrentSends, err = getInt(envChipIngressMaxConcurrentSends)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envChipIngressMaxConcurrentSends, err)
}
pkg/loop/config.go:746
getUintparses into auint64then casts touintwithout checking for overflow. On 32-bit platforms (or with very large env values), this can wrap/truncate and potentially produce an invalid buffer/batch size downstream. Consider adding a platform-size overflow guard before converting.
u, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
return uint(u), nil
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
pkg/loop/config.go:609
- CL_CHIP_INGRESS_MAX_CONCURRENT_SENDS is parsed via getInt but negative values are currently accepted. A negative value will later panic when used to size the semaphore channel in batch.WithMaxConcurrentSends. Please reject negative values during env parsing (0 can remain the sentinel for "use defaults").
return fmt.Errorf("failed to parse %s: %w", envChipIngressDurableEmitterEnabled, err)
}
e.ChipIngressBufferSize, err = getUint(envChipIngressBufferSize)
if err != nil {
pkg/loop/config.go:746
- getUint currently allows values up to the full uint range (e.g., 2^64-1 on 64-bit), but downstream these chip-ingress sizes are cast to int when wiring the batch client. Values larger than MaxInt will overflow to negative and can cause panics when used as channel capacities. Consider bounding parsed values to MaxInt and returning an error when exceeded.
s := os.Getenv(envKey)
if s == "" {
return 0, nil
}
return time.ParseDuration(s)
| maxGRPCRequestSize := cfg.ChipIngressMaxGRPCRequestSize | ||
| if maxGRPCRequestSize == 0 { | ||
| maxGRPCRequestSize = defaults.ChipIngressMaxGRPCRequestSize | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
pkg/loop/config.go:619
CL_CHIP_INGRESS_MAX_CONCURRENT_SENDScan currently be set to a negative value, which will flow intobatch.WithMaxConcurrentSendsand panic when it creates a channel with negative capacity. Consider rejecting negative values during env parsing (similar to the existingChipIngressMaxGRPCRequestSizevalidation).
e.ChipIngressMaxConcurrentSends, err = getInt(envChipIngressMaxConcurrentSends)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envChipIngressMaxConcurrentSends, err)
}
| // Chip Ingress Batch Emitter | ||
| ChipIngressBatchEmitterEnabled bool // When true, use batch emitter; when false (default), use legacy per-event emitter | ||
| ChipIngressBufferSize uint // Message buffer size (default 1000) | ||
| ChipIngressMaxBatchSize uint // Max events per PublishBatch call (default 500) | ||
| ChipIngressSendInterval time.Duration // Flush interval (default 100ms) | ||
| ChipIngressSendTimeout time.Duration // Timeout per PublishBatch call (default 3s) | ||
| ChipIngressDrainTimeout time.Duration // Max time to flush remaining events on shutdown (default 10s) | ||
| ChipIngressBufferSize uint // Message buffer size (default 10000) | ||
| ChipIngressMaxBatchSize uint // Max events per PublishBatch call (default 1000) | ||
| ChipIngressSendInterval time.Duration // Flush interval (default 500ms) | ||
| ChipIngressSendTimeout time.Duration // Timeout per PublishBatch call (default 10s) | ||
| ChipIngressDrainTimeout time.Duration // Max time to flush remaining events on shutdown (default 30s) | ||
| ChipIngressMaxConcurrentSends int // Max concurrent PublishBatch calls (default 10) | ||
| ChipIngressMaxGRPCRequestSize int // Max serialized PublishBatch request size in bytes (default 10 MiB) |
Summary
Add 7 chip-ingress batch configuration keys to
beholder.Configand wire them through the batch emitter and LOOP plugin env config.This PR also updates the batch emitter defaults based on staging/prod capacity analysis and fixes the histogram bucket schema for
chip_ingress.batch.request_size_*.Default changes
Batches are interval-bound at steady state (p99 ~10 msgs/batch), so
MaxBatchSizeis a burst-safety ceiling rather than the active flush lever.Changes
pkg/beholder/config.go: addedChipIngressMaxGRPCRequestSize+ updated defaultspkg/beholder/batch_emitter_service.go: wiredWithMaxGRPCRequestSizepkg/chipingress/batch/client.go: fixed histogram bucket boundaries forrequest_size_messagesandrequest_size_bytespkg/loop/config.go: addedCL_CHIP_INGRESS_*env vars,EnvConfigfields, serialization/parsingpkg/loop/server.go: wired env fields intobeholder.ConfigRelated PRs
Testing
go test ./pkg/loop/... ./pkg/beholder/... ./pkg/chipingress/...Notes
Co-authored-by: Cursorattribution has been removed from the commit history.