Skip to content

Commit 9e0673a

Browse files
agoallikmaavyagh
andcommitted
[memorylimiter] Add garbage_collector config
Co-authored-by: Shubham Sharma <vyagh.vy@gmail.com>
1 parent 52e6bf4 commit 9e0673a

12 files changed

Lines changed: 112 additions & 2 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
change_type: enhancement
2+
3+
component: processor/memory_limiter
4+
5+
note: Add `garbage_collector` config section to allow disabling forced garbage collection via `enabled` field.
6+
7+
issues: [15081]
8+
9+
change_logs: [user]

extension/memorylimiterextension/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/stretchr/testify v1.11.1
77
go.opentelemetry.io/collector/component v1.63.0
88
go.opentelemetry.io/collector/component/componenttest v0.157.0
9+
go.opentelemetry.io/collector/config/configoptional v1.63.0
910
go.opentelemetry.io/collector/confmap v1.63.0
1011
go.opentelemetry.io/collector/extension v1.63.0
1112
go.opentelemetry.io/collector/extension/extensionmiddleware v0.157.0
@@ -88,3 +89,5 @@ replace go.opentelemetry.io/collector/internal/componentalias => ../../internal/
8889
replace go.opentelemetry.io/collector/pipeline => ../../pipeline
8990

9091
replace go.opentelemetry.io/collector/component/componentstatus => ../../component/componentstatus
92+
93+
replace go.opentelemetry.io/collector/config/configoptional => ../../config/configoptional

extension/memorylimiterextension/memorylimiter_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.uber.org/zap"
1515

1616
"go.opentelemetry.io/collector/component/componenttest"
17+
"go.opentelemetry.io/collector/config/configoptional"
1718
"go.opentelemetry.io/collector/internal/memorylimiter"
1819
"go.opentelemetry.io/collector/internal/memorylimiter/iruntime"
1920
)
@@ -33,6 +34,7 @@ func TestMemoryPressureResponse(t *testing.T) {
3334
CheckInterval: time.Second,
3435
MemoryLimitPercentage: 50,
3536
MemorySpikePercentage: 1,
37+
GarbageCollector: configoptional.Some(memorylimiter.GarbageCollectorConfig{}),
3638
},
3739
memAlloc: 800,
3840
expectError: false,
@@ -43,6 +45,7 @@ func TestMemoryPressureResponse(t *testing.T) {
4345
CheckInterval: time.Second,
4446
MemoryLimitPercentage: 50,
4547
MemorySpikePercentage: 1,
48+
GarbageCollector: configoptional.Some(memorylimiter.GarbageCollectorConfig{}),
4649
},
4750
memAlloc: 1800,
4851
expectError: true,
@@ -53,6 +56,7 @@ func TestMemoryPressureResponse(t *testing.T) {
5356
CheckInterval: time.Second,
5457
MemoryLimitPercentage: 50,
5558
MemorySpikePercentage: 10,
59+
GarbageCollector: configoptional.Some(memorylimiter.GarbageCollectorConfig{}),
5660
},
5761
memAlloc: 800,
5862
expectError: false,
@@ -63,6 +67,7 @@ func TestMemoryPressureResponse(t *testing.T) {
6367
CheckInterval: time.Second,
6468
MemoryLimitPercentage: 50,
6569
MemorySpikePercentage: 11,
70+
GarbageCollector: configoptional.Some(memorylimiter.GarbageCollectorConfig{}),
6671
},
6772
memAlloc: 800,
6873
expectError: true,

internal/memorylimiter/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"go.opentelemetry.io/collector/component"
11+
"go.opentelemetry.io/collector/config/configoptional"
1112
)
1213

1314
var (
@@ -25,6 +26,9 @@ var (
2526
"'limit_percentage' and 'spike_limit_percentage' must be greater than zero and less than or equal to hundred")
2627
)
2728

29+
// GarbageCollectorConfig defines configuration for garbage collection.
30+
type GarbageCollectorConfig struct{}
31+
2832
// Config defines configuration for memory memoryLimiter processor.
2933
type Config struct {
3034
// CheckInterval is the time between measurements of memory usage for the
@@ -58,6 +62,9 @@ type Config struct {
5862
// Set to 0 to disable the exponential backoff on this path.
5963
MaxGCIntervalWhenHardLimited time.Duration `mapstructure:"max_gc_interval_when_hard_limited"`
6064

65+
// GarbageCollector configuration section. Use enabled: false to disable the memory limiter's forced garbage collection.
66+
GarbageCollector configoptional.Optional[GarbageCollectorConfig] `mapstructure:"garbage_collector"`
67+
6168
// MemoryLimitMiB is the maximum amount of memory, in MiB, targeted to be
6269
// allocated by the process.
6370
MemoryLimitMiB uint32 `mapstructure:"limit_mib"`
@@ -82,6 +89,7 @@ func NewDefaultConfig() *Config {
8289
MinGCIntervalWhenSoftLimited: 10 * time.Second,
8390
MaxGCIntervalWhenSoftLimited: 30 * time.Second,
8491
MaxGCIntervalWhenHardLimited: 30 * time.Second,
92+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
8593
}
8694
}
8795

internal/memorylimiter/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313

14+
"go.opentelemetry.io/collector/config/configoptional"
1415
"go.opentelemetry.io/collector/confmap/confmaptest"
1516
)
1617

@@ -24,6 +25,7 @@ func TestUnmarshalConfig(t *testing.T) {
2425
CheckInterval: 5 * time.Second,
2526
MemoryLimitMiB: 4000,
2627
MemorySpikeLimitMiB: 500,
28+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
2729
}, cfg)
2830
}
2931

internal/memorylimiter/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
go.opentelemetry.io/collector/component v1.63.0
99
go.opentelemetry.io/collector/component/componentstatus v0.157.0
1010
go.opentelemetry.io/collector/component/componenttest v0.157.0
11+
go.opentelemetry.io/collector/config/configoptional v1.63.0
1112
go.opentelemetry.io/collector/confmap v1.63.0
1213
go.uber.org/goleak v1.3.0
1314
go.uber.org/zap v1.28.0
@@ -68,3 +69,5 @@ replace go.opentelemetry.io/collector/component/componenttest => ../../component
6869
replace go.opentelemetry.io/collector/component/componentstatus => ../../component/componentstatus
6970

7071
replace go.opentelemetry.io/collector/pipeline => ../../pipeline
72+
73+
replace go.opentelemetry.io/collector/config/configoptional => ../../config/configoptional

internal/memorylimiter/memorylimiter.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type MemoryLimiter struct {
5555
maxGCIntervalWhenSoftLimited time.Duration
5656
maxGCIntervalWhenHardLimited time.Duration
5757
lastGCDone time.Time
58+
gcDisabled bool
5859

5960
// The functions to read the mem values and run GC are set as a reference to help with
6061
// testing different values.
@@ -121,6 +122,7 @@ func NewMemoryLimiter(cfg *Config, logger *zap.Logger) (*MemoryLimiter, error) {
121122
maxGCIntervalWhenHardLimited: cfg.MaxGCIntervalWhenHardLimited,
122123
lastStats: &runtime.MemStats{},
123124
lastGCDone: time.Now(),
125+
gcDisabled: !cfg.GarbageCollector.HasValue(),
124126
readMemStatsFn: ReadMemStatsFn,
125127
runGCFn: runtime.GC,
126128
logger: logger,
@@ -278,7 +280,7 @@ func (ml *MemoryLimiter) CheckMemLimits() {
278280
gateInterval := max(ml.minGCIntervalWhenHardLimited, ml.currentHardGCInterval)
279281
// We are above hard limit, do a GC if it wasn't done recently and see if
280282
// it brings memory usage below the soft limit.
281-
if time.Since(ml.lastGCDone) > gateInterval {
283+
if !ml.gcDisabled && time.Since(ml.lastGCDone) > gateInterval {
282284
ml.logger.Warn("Memory usage is above hard limit. Forcing a GC.", memstatToZapField(ms))
283285
ms = ml.doGCandReadMemStats()
284286
// Check the limit again to see if GC helped.
@@ -288,7 +290,7 @@ func (ml *MemoryLimiter) CheckMemLimits() {
288290
gateInterval := max(ml.minGCIntervalWhenSoftLimited, ml.currentSoftGCInterval)
289291
// We are above soft limit, do a GC if it wasn't done recently and see if
290292
// it brings memory usage below the soft limit.
291-
if time.Since(ml.lastGCDone) > gateInterval {
293+
if !ml.gcDisabled && time.Since(ml.lastGCDone) > gateInterval {
292294
ml.logger.Info("Memory usage is above soft limit. Forcing a GC.", memstatToZapField(ms))
293295
ms = ml.doGCandReadMemStats()
294296
// Check the limit again to see if GC helped.

internal/memorylimiter/memorylimiter_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"go.opentelemetry.io/collector/component"
1717
"go.opentelemetry.io/collector/component/componentstatus"
1818
"go.opentelemetry.io/collector/component/componenttest"
19+
"go.opentelemetry.io/collector/config/configoptional"
1920
"go.opentelemetry.io/collector/internal/memorylimiter/iruntime"
2021
)
2122

@@ -39,6 +40,7 @@ func TestMemoryPressureResponse(t *testing.T) {
3940
CheckInterval: 1 * time.Minute,
4041
MemoryLimitMiB: 1024,
4142
MemorySpikeLimitMiB: 0,
43+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
4244
}
4345
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
4446
require.NoError(t, err)
@@ -200,6 +202,7 @@ func TestCheckMemLimitsHealthEvents(t *testing.T) {
200202
CheckInterval: 1 * time.Minute,
201203
MemoryLimitMiB: 50,
202204
MemorySpikeLimitMiB: 10,
205+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
203206
}, zap.NewNop())
204207
require.NoError(t, err)
205208
host := &mockHost{}
@@ -238,6 +241,7 @@ func TestCallGCWhenSoftLimit(t *testing.T) {
238241
MaxGCIntervalWhenHardLimited: 30 * time.Second,
239242
MemoryLimitMiB: 50,
240243
MemorySpikeLimitMiB: 10,
244+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
241245
},
242246
memAllocMiB: [2]uint64{45, 45},
243247
numGCs: 1,
@@ -251,6 +255,7 @@ func TestCallGCWhenSoftLimit(t *testing.T) {
251255
MaxGCIntervalWhenHardLimited: 30 * time.Second,
252256
MemoryLimitMiB: 50,
253257
MemorySpikeLimitMiB: 10,
258+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
254259
},
255260
memAllocMiB: [2]uint64{45, 45},
256261
numGCs: 2,
@@ -264,6 +269,7 @@ func TestCallGCWhenSoftLimit(t *testing.T) {
264269
MaxGCIntervalWhenHardLimited: 30 * time.Second,
265270
MemoryLimitMiB: 50,
266271
MemorySpikeLimitMiB: 10,
272+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
267273
},
268274
memAllocMiB: [2]uint64{55, 55},
269275
numGCs: 1,
@@ -277,6 +283,7 @@ func TestCallGCWhenSoftLimit(t *testing.T) {
277283
MaxGCIntervalWhenHardLimited: 30 * time.Second,
278284
MemoryLimitMiB: 50,
279285
MemorySpikeLimitMiB: 10,
286+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
280287
},
281288
memAllocMiB: [2]uint64{55, 55},
282289
numGCs: 2,
@@ -291,10 +298,39 @@ func TestCallGCWhenSoftLimit(t *testing.T) {
291298
MaxGCIntervalWhenHardLimited: 30 * time.Second,
292299
MemoryLimitMiB: 50,
293300
MemorySpikeLimitMiB: 10,
301+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
294302
},
295303
memAllocMiB: [2]uint64{45, 55},
296304
numGCs: 2,
297305
},
306+
{
307+
name: "GC disabled when above soft limit",
308+
mlCfg: &Config{
309+
CheckInterval: 1 * time.Minute,
310+
MinGCIntervalWhenSoftLimited: 0,
311+
MaxGCIntervalWhenSoftLimited: 30 * time.Second,
312+
MaxGCIntervalWhenHardLimited: 30 * time.Second,
313+
MemoryLimitMiB: 50,
314+
MemorySpikeLimitMiB: 10,
315+
GarbageCollector: configoptional.None[GarbageCollectorConfig](),
316+
},
317+
memAllocMiB: [2]uint64{45, 45},
318+
numGCs: 0,
319+
},
320+
{
321+
name: "GC disabled when above hard limit",
322+
mlCfg: &Config{
323+
CheckInterval: 1 * time.Minute,
324+
MinGCIntervalWhenHardLimited: 0,
325+
MaxGCIntervalWhenSoftLimited: 30 * time.Second,
326+
MaxGCIntervalWhenHardLimited: 30 * time.Second,
327+
MemoryLimitMiB: 50,
328+
MemorySpikeLimitMiB: 10,
329+
GarbageCollector: configoptional.None[GarbageCollectorConfig](),
330+
},
331+
memAllocMiB: [2]uint64{55, 55},
332+
numGCs: 0,
333+
},
298334
}
299335
for _, tt := range tests {
300336
t.Run(tt.name, func(t *testing.T) {
@@ -354,6 +390,7 @@ func TestGCBackoffWhenIneffective(t *testing.T) {
354390
MaxGCIntervalWhenHardLimited: 30 * time.Second,
355391
MemoryLimitMiB: 50,
356392
MemorySpikeLimitMiB: 10,
393+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
357394
}
358395
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
359396
require.NoError(t, err)
@@ -406,6 +443,7 @@ func TestGCBackoffResetOnRecovery(t *testing.T) {
406443
MaxGCIntervalWhenHardLimited: 30 * time.Second,
407444
MemoryLimitMiB: 50,
408445
MemorySpikeLimitMiB: 10,
446+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
409447
}
410448
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
411449
require.NoError(t, err)
@@ -458,6 +496,7 @@ func TestGCBackoffEarlyResetWhenMemoryBecomesReclaimable(t *testing.T) {
458496
MaxGCIntervalWhenHardLimited: 30 * time.Second,
459497
MemoryLimitMiB: 50,
460498
MemorySpikeLimitMiB: 10,
499+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
461500
}
462501
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
463502
require.NoError(t, err)
@@ -509,6 +548,7 @@ func TestGCBackoffEarlyResetAboveHardLimit(t *testing.T) {
509548
MaxGCIntervalWhenHardLimited: 30 * time.Second,
510549
MemoryLimitMiB: 50,
511550
MemorySpikeLimitMiB: 10,
551+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
512552
}
513553
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
514554
require.NoError(t, err)
@@ -555,6 +595,7 @@ func TestNextBackoffInterval(t *testing.T) {
555595
MaxGCIntervalWhenHardLimited: 30 * time.Second,
556596
MemoryLimitMiB: 100,
557597
MemorySpikeLimitMiB: 5,
598+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
558599
}
559600
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
560601
require.NoError(t, err)
@@ -613,6 +654,7 @@ func TestCheckLimitAndBackoff(t *testing.T) {
613654
MaxGCIntervalWhenHardLimited: 30 * time.Second,
614655
MemoryLimitMiB: 100,
615656
MemorySpikeLimitMiB: 5,
657+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
616658
}
617659
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
618660
require.NoError(t, err)
@@ -679,6 +721,7 @@ func TestCheckLimitAndBackoff(t *testing.T) {
679721
MaxGCIntervalWhenHardLimited: 0,
680722
MemoryLimitMiB: 100,
681723
MemorySpikeLimitMiB: 5,
724+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
682725
}
683726
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
684727
require.NoError(t, err)
@@ -701,6 +744,7 @@ func TestBackoffDisabledOptOut(t *testing.T) {
701744
MaxGCIntervalWhenHardLimited: 0,
702745
MemoryLimitMiB: 50,
703746
MemorySpikeLimitMiB: 10,
747+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
704748
}
705749
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
706750
require.NoError(t, err)
@@ -736,6 +780,7 @@ func TestPerPathBackoffIsIndependent(t *testing.T) {
736780
MaxGCIntervalWhenHardLimited: 0,
737781
MemoryLimitMiB: 50,
738782
MemorySpikeLimitMiB: 10,
783+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
739784
}
740785
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
741786
require.NoError(t, err)
@@ -774,6 +819,7 @@ func TestNewDefaultConfigEnablesBackoffCap(t *testing.T) {
774819
cfg := NewDefaultConfig()
775820
assert.Equal(t, 30*time.Second, cfg.MaxGCIntervalWhenSoftLimited, "soft backoff cap should default to 30s")
776821
assert.Equal(t, 30*time.Second, cfg.MaxGCIntervalWhenHardLimited, "hard backoff cap should default to 30s")
822+
assert.True(t, cfg.GarbageCollector.HasValue(), "garbage collector should be enabled by default")
777823
}
778824

779825
func TestBackoffGrowsBothPathsInLockstep(t *testing.T) {
@@ -798,6 +844,7 @@ func TestBackoffGrowsBothPathsInLockstep(t *testing.T) {
798844
MaxGCIntervalWhenHardLimited: 30 * time.Second,
799845
MemoryLimitMiB: 50,
800846
MemorySpikeLimitMiB: 10,
847+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
801848
}
802849
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
803850
require.NoError(t, err)
@@ -842,6 +889,7 @@ func TestGCEffectivenessWhenPressureResolved(t *testing.T) {
842889
MaxGCIntervalWhenHardLimited: 30 * time.Second,
843890
MemoryLimitMiB: 100,
844891
MemorySpikeLimitMiB: 5,
892+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
845893
}
846894
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
847895
require.NoError(t, err)
@@ -900,6 +948,7 @@ func TestGCRecovery(t *testing.T) {
900948
CheckInterval: 1 * time.Minute,
901949
MemoryLimitMiB: 50,
902950
MemorySpikeLimitMiB: 10,
951+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
903952
}, zap.NewNop())
904953
require.NoError(t, err)
905954

@@ -933,6 +982,7 @@ func TestStart(t *testing.T) {
933982
MinGCIntervalWhenSoftLimited: 10 * time.Second,
934983
MemoryLimitMiB: 50,
935984
MemorySpikeLimitMiB: 10,
985+
GarbageCollector: configoptional.Some(GarbageCollectorConfig{}),
936986
}
937987
ml, err := NewMemoryLimiter(cfg, zap.NewNop())
938988
require.NoError(t, err)

internal/memorylimiter/testdata/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ limit_mib: 4000
1111

1212
# The maximum, in MiB, spike expected between the measurements of memory usage.
1313
spike_limit_mib: 500
14+
15+
# Set enabled: false to disable forced garbage collection.
16+
garbage_collector:
17+
enabled: true

processor/memorylimiterprocessor/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ of `min_gc_interval_when_soft_limited` (which is every check interval if min
150150
is also `0`).
151151
- `max_gc_interval_when_hard_limited` (default = `30s`): Same as above but for
152152
hard-limited mode. Set to `0` to disable.
153+
- `garbage_collector` (optional section, enabled by default): Controls forced
154+
GC calls. Set `enabled: false` to disable forced garbage collection. The
155+
processor will still refuse data when memory limits are exceeded.
153156

154157
Examples:
155158

0 commit comments

Comments
 (0)