Skip to content

Goroutine leak in v3.4.0 when using OnEviction handlers #185

@haydenmeadeanz

Description

@haydenmeadeanz

Summary

ttlcache v3.4.0 introduces a goroutine leak when OnEviction handlers are registered. The cache background goroutine is not properly cleaned up when the cleanup function returned by OnEviction() is called, even though cache.Stop() is also called.

Environment

  • ttlcache version: v3.4.0
  • Go version: go1.24

Expected Behavior

When an OnEviction handler is registered and later cleaned up, no goroutines should leak. The cleanup function returned by OnEviction() should ensure all related goroutines are properly terminated.

Actual Behavior

Goroutines leak when OnEviction handlers are used, even when the cleanup function is called before cache.Stop(). The leak is detected by go.uber.org/goleak and shows goroutines stuck in the cache's Start() method select loop.

Minimal reproduction code:

package main

import (
    "context"
    "testing"
    "time"

    "go.uber.org/goleak"
    "github.com/jellydator/ttlcache/v3"
)

func TestOnEvictionLeak(t *testing.T) {
    defer goleak.VerifyNone(t)
    
    cache := ttlcache.New(ttlcache.WithTTL[string, string](10 * time.Millisecond))
    
    cleanup := cache.OnEviction(func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, string]) { })
    
    go cache.Start()
    
    cache.Set("test", "value", 5*time.Millisecond)
    
    cleanup()  
    cache.Stop() 
    
    time.Sleep(time.Second)
}

Expected result:

Test passes with no goroutine leaks detected.

Actual result:

found unexpected goroutines:
[Goroutine X in state select, with github.com/jellydator/ttlcache/v3.(*Cache[...]).Start on top of the stack:
github.com/jellydator/ttlcache/v3.(*Cache[...]).Start(0x...)
    /go/pkg/mod/github.com/jellydator/ttlcache/[email protected]/cache.go:689 +0x1fc
created by ...
]

Analysis

The issue appears to be related to the interaction between:

  1. The OnEviction handler system introduced in v3.4.0
  2. The cache background goroutine cleanup in the Stop() method

Working in v3.3.0

The same test pattern works fine in v3.3.0

Would appreciate investigation into the goroutine cleanup coordination between the OnEviction system and the main cache background goroutine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions