Skip to content
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

[TT-13723] Update to Go 1.23 #6812

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

[TT-13723] Update to Go 1.23 #6812

wants to merge 7 commits into from

Conversation

titpetric
Copy link
Contributor

@titpetric titpetric commented Dec 23, 2024

User description

TT-13723
Summary Update to Go 1.23
Type Story Story
Status In Dev
Points N/A
Labels -

https://tyktech.atlassian.net/browse/TT-13723

It seems some tests detect goroutine leaks now. The detected goroutines leaked have been listed in the ignores of a debug2.Record; both goroutine leak tests detect goroutines in background reliably. Both are flaky otherwise, this passes a -count=100 run, with and without -race.


PR Type

Enhancement, Tests, Configuration changes


Description

  • Introduced debug2.Record to enhance goroutine state tracking and comparison.
  • Improved goroutine leak detection tests using debug2.Record.
  • Added unit and benchmark tests for debug2.Record.
  • Updated CI workflows to use Go 1.23.x.
  • Simplified Dockerfile by switching to Go 1.23-bullseye base image and optimizing build steps.
  • Updated plugin compiler and release workflows to support Go 1.23.
  • Enhanced Taskfile to dynamically use the Go version from go.mod.
  • Bumped Go version in go.mod to 1.23.4.

Changes walkthrough 📝

Relevant files
Tests
2 files
gateway_test.go
Improved goroutine leak detection in tests.                           

gateway/gateway_test.go

  • Enhanced goroutine leak tests with debug2.Record for better
    reliability.
  • Introduced newRecord helper function to manage ignored goroutines.
  • Updated assertions to use debug2.Record for goroutine count
    validation.
  • +37/-15 
    goroutine_test.go
    Added unit and benchmark tests for `debug2.Record`.           

    internal/debug2/goroutine_test.go

  • Added unit tests for debug2.Record to validate goroutine tracking.
  • Included benchmark tests for performance evaluation.
  • Verified goroutine cleanup using Since method.
  • +103/-0 
    Enhancement
    4 files
    goroutine.go
    Introduced `debug2.Record` for goroutine state tracking. 

    internal/debug2/goroutine.go

  • Added debug2.Record to capture and compare goroutine states.
  • Implemented methods for parsing, counting, and filtering goroutines.
  • Introduced functionality to ignore specific goroutines during
    comparison.
  • +124/-0 
    Dockerfile
    Simplified Dockerfile with Go 1.23 base image.                     

    Dockerfile

  • Simplified Dockerfile by using Go 1.23-bullseye base image.
  • Removed redundant Python installation steps.
  • Optimized build process with caching for Go modules.
  • +11/-51 
    Taskfile.yml
    Enhanced Taskfile to support dynamic Go version.                 

    Taskfile.yml

    • Added dynamic Go version argument for Docker builds.
    +1/-1     
    go.mod
    Bumped Go version in module to 1.23.4.                                     

    go.mod

    • Updated Go version requirement to 1.23.4.
    +1/-1     
    Configuration changes
    4 files
    ci-tests.yml
    Updated CI workflow to use Go 1.23.x.                                       

    .github/workflows/ci-tests.yml

    • Updated Go version in CI matrix to 1.23.x.
    +1/-1     
    plugin-compiler-build.yml
    Updated plugin compiler workflow for Go 1.23.                       

    .github/workflows/plugin-compiler-build.yml

  • Updated base image to use Go 1.23-bullseye.
  • Fixed BASE_IMAGE argument in Docker build steps.
  • +3/-3     
    release.yml
    Updated release workflow to support Go 1.23.                         

    .github/workflows/release.yml

  • Updated Go version in release workflow to 1.23-bullseye.
  • Adjusted Docker build conditions for the new Go version.
  • +11/-11 
    Dockerfile
    Updated plugin compiler Dockerfile for Go 1.23.                   

    ci/images/plugin-compiler/Dockerfile

    • Updated base image to Go 1.23-bullseye.
    +1/-1     

    💡 PR-Agent usage: Comment /help "your question" on any pull request to receive relevant information

    @buger
    Copy link
    Member

    buger commented Dec 23, 2024

    I'm a bot and I 👍 this PR title. 🤖

    Copy link
    Contributor

    API Changes

    no api changes detected

    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Possible Flaky Test

    The new goroutine leak tests rely on time.Sleep and runtime.GC() for synchronization, which might lead to flaky behavior in different environments. Consider using more robust synchronization mechanisms.

    func TestReloadGoroutineLeakWithTest(t *testing.T) {
    	newRecord := func() *debug2.Record {
    		result := debug2.NewRecord()
    		result.SetIgnores([]string{
    			"runtime/pprof.writeRuntimeProfile",
    			"/root/go/pkg/mod/github.com/!tyk!technologies/[email protected]/memorycache/cache.go:69",
    			"/root/go/pkg/mod/github.com/pmylund/[email protected]+incompatible/cache.go:1079",
    			"/root/tyk/tyk/gateway/distributed_rate_limiter.go:31",
    			"/root/tyk/tyk/gateway/redis_signals.go:68",
    		})
    
    		return result
    	}
    
    	before := newRecord()
    
    	ts := StartTest(nil)
    	ts.Close()
    
    	time.Sleep(100 * time.Millisecond)
    	runtime.GC()
    
    	final := newRecord().Since(before)
    	assert.Equal(t, 0, final.Count(), "final count not zero: %s", final)
    }
    
    func TestReloadGoroutineLeakWithCircuitBreaker(t *testing.T) {
    	ts := StartTest(nil)
    	t.Cleanup(ts.Close)
    
    	newRecord := func() *debug2.Record {
    		result := debug2.NewRecord()
    		result.SetIgnores([]string{
    			"runtime/pprof.writeRuntimeProfile",
    			"/root/tyk/tyk/gateway/reverse_proxy.go:223",
    			"/root/tyk/tyk/gateway/api_definition.go:1025",
    			"/root/tyk/tyk/gateway/distributed_rate_limiter.go:31",
    			"/root/go/pkg/mod/github.com/pmylund/[email protected]+incompatible/cache.go:1079",
    			"/root/go/pkg/mod/github.com/!tyk!technologies/[email protected]+incompatible/circuitbreaker.go:202",
    		})
    
    		return result
    	}
    
    	globalConf := ts.Gw.GetConfig()
    	globalConf.EnableJSVM = false
    	ts.Gw.SetConfig(globalConf)
    
    	stage1 := newRecord()
    
    	specs := ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
    		spec.Proxy.ListenPath = "/"
    		UpdateAPIVersion(spec, "v1", func(version *apidef.VersionInfo) {
    			version.ExtendedPaths = apidef.ExtendedPathsSet{
    				CircuitBreaker: []apidef.CircuitBreakerMeta{
    					{
    						Path:                 "/",
    						Method:               http.MethodGet,
    						ThresholdPercent:     0.5,
    						Samples:              5,
    						ReturnToServiceAfter: 10,
    					},
    				},
    			}
    		})
    	})
    
    	ts.Gw.LoadAPI(specs...) // just doing globalGateway.DoReload() doesn't load anything as BuildAndLoadAPI cleans up folder with API specs
    
    	time.Sleep(100 * time.Millisecond)
    	runtime.GC()
    
    	final := newRecord().Since(stage1)
    	assert.Equal(t, 0, final.Count(), "final count not zero: %s", final)
    }
    Performance Concern

    The parseGoroutines function processes goroutine dumps line by line and performs multiple string operations, which might become a performance bottleneck for large dumps. Consider optimizing this logic.

    func (r *Record) parseGoroutines() map[string][]string {
    	goroutines := make(map[string][]string)
    	var currentHeader string
    	var currentStack []string
    	toDelete := []string{}
    	lines := strings.Split(r.buffer.String(), "\n")
    
    	for _, line := range lines {
    		var skip bool
    		for _, ign := range r.ignores {
    			if strings.Contains(line, ign) {
    				skip = true
    				break
    			}
    		}
    
    		if skip {
    			toDelete = append(toDelete, currentHeader)
    		}
    
    		if headerMatchRe.MatchString(line) {
    			// Save the previous goroutine and reset
    			if currentHeader != "" {
    				goroutines[currentHeader] = currentStack
    			}
    			currentHeader = line
    			currentStack = []string{line}
    		} else if currentHeader != "" {
    			// Add stack trace lines to the current goroutine
    			currentStack = append(currentStack, line)
    		}
    	}
    
    	// Save the last goroutine
    	if currentHeader != "" {
    		goroutines[currentHeader] = currentStack
    	}
    
    	for _, key := range toDelete {
    		delete(goroutines, key)
    	}
    
    	return goroutines

    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    General
    Add a maximum iteration limit to prevent infinite loops during goroutine cleanup checks

    Add a timeout or fail-safe mechanism in the loop waiting for goroutines to finish to
    prevent infinite loops in case of unexpected behavior.

    internal/debug2/goroutine_test.go [40-57]

    -for {
    +for i := 0; i < 50; i++ { // Limit iterations to prevent infinite loop
         time.Sleep(100 * time.Millisecond)
         runtime.GC()
         time.Sleep(10 * time.Millisecond)
         ...
         if remainingGoroutines.Count() == 0 {
             break
         }
     }
    Suggestion importance[1-10]: 8

    Why: Adding a maximum iteration limit is a significant improvement to prevent potential infinite loops during goroutine cleanup checks, enhancing reliability and robustness of the test.

    8
    Remove unnecessary build dependencies to reduce the final Docker image size

    Add a cleanup step to remove unnecessary build dependencies after the build process
    to reduce the final image size.

    Dockerfile [6]

    -RUN apt update && apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl wget libbz2-dev -y
    +RUN apt update && apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl wget libbz2-dev -y && \
    +    apt-get remove --purge -y build-essential && apt-get autoremove -y && apt-get clean
    Suggestion importance[1-10]: 7

    Why: Removing unnecessary build dependencies is a good practice to reduce the final Docker image size, improving efficiency and security. This suggestion aligns well with the PR's context and goals.

    7
    Clear the toDelete slice after use to prevent potential memory leaks or unintended side effects

    Ensure that the toDelete slice is cleared after processing to avoid potential memory
    leaks or unintended behavior in subsequent calls to parseGoroutines.

    internal/debug2/goroutine.go [44-79]

     toDelete := []string{}
     ...
     for _, key := range toDelete {
         delete(goroutines, key)
     }
    +toDelete = nil
    Suggestion importance[1-10]: 3

    Why: Clearing the toDelete slice after use is a minor improvement for memory management, but it is not critical since Go's garbage collector would handle it. The impact on functionality is minimal.

    3

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants