Skip to content

Adds some examples #11

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"context"
"fmt"
"github.com/openshift-online/async-routine/opid"
"log/slog"
"os"
"runtime/pprof"
"time"
)

func main() {
slog.Info("Program started")

for i := 0; i < 10; i++ {
foo(opid.NewContext())
}

// Wait enough time to have some routine started
time.Sleep(4 * time.Second)

pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
fmt.Scanln()
}

func foo(ctx context.Context) {
slog.Info("foo() started",
"opid", opid.FromContext(ctx))
bar(ctx)
slog.Info("foo() ended",
"opid", opid.FromContext(ctx))
}

func bar(ctx context.Context) {
slog.Info("bar() started",
"opid", opid.FromContext(ctx))
go parentGoroutine(ctx)
slog.Info("bar() ended",
"opid", opid.FromContext(ctx))
}

func parentGoroutine(ctx context.Context) {
slog.Info("parentGoroutine() started",
"opid", opid.FromContext(ctx))

go stuckInSelect()
time.Sleep(500 * time.Millisecond)
slog.Info("parentGoroutine() ended",
"opid", opid.FromContext(ctx))
}

func stuckInSelect() {
slog.Info("stuckInSelect() started")
select {}
slog.Info("stuckInSelect() ended")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package main

import (
"context"
"github.com/openshift-online/async-routine"
"github.com/openshift-online/async-routine/opid"
"log/slog"
"time"
)

var _ async.RoutinesObserver = (*exampleRoutineObserver)(nil)

func main() {
slog.Info("Program started")

// Setup the AsyncRoutineManager
async.Manager(
// Take a snapshot every 2 seconds
async.WithSnapshottingInterval(2 * time.Second)).
// Start the routine monitor
Monitor().Start()

// Add our custom observer to the list of routine observers
_ = async.Manager().AddObserver(&exampleRoutineObserver{})

for i := 0; i < 10; i++ {
foo(opid.NewContext())
}

// Wait enough time to have some routine started
time.Sleep(4 * time.Second)
}

func foo(ctx context.Context) {
slog.Info("foo() started",
"opid", opid.FromContext(ctx))
bar(ctx)
slog.Info("foo() ended",
"opid", opid.FromContext(ctx))
}

func bar(ctx context.Context) {
slog.Info("bar() started",
"opid", opid.FromContext(ctx))
async.NewAsyncRoutine("parent go routine", ctx,
func() {
parentGoroutine(ctx)
}).
Timebox(2 * time.Second).
Run()
slog.Info("bar() ended",
"opid", opid.FromContext(ctx))
}

func parentGoroutine(ctx context.Context) {
slog.Info("parentGoroutine() started",
"opid", opid.FromContext(ctx))

async.NewAsyncRoutine("stuck in select", ctx, stuckInSelect).
Timebox(2 * time.Second).
Run()
time.Sleep(500 * time.Millisecond)

slog.Info("parentGoroutine() ended",
"opid", opid.FromContext(ctx))
}

func stuckInSelect() {
slog.Info("parentGoroutine() started")
select {}
slog.Info("parentGoroutine() ended")
}

type exampleRoutineObserver struct{}

func (e exampleRoutineObserver) RoutineStarted(routine async.AsyncRoutine) {
slog.Info("Routine started",
"name", routine.Name(),
"opid", routine.OpId(),
"parent-opid", routine.OriginatorOpId(),
)
}

func (e exampleRoutineObserver) RoutineFinished(routine async.AsyncRoutine) {
slog.Info("Routine finished",
"name", routine.Name(),
"opid", routine.OpId(),
"parent-opid", routine.OriginatorOpId(),
)
}

func (e exampleRoutineObserver) RoutineExceededTimebox(routine async.AsyncRoutine) {
slog.Warn("Routine exceeded timebox",
"name", routine.Name(),
"opid", routine.OpId(),
"parent-opid", routine.OriginatorOpId(),
"startedAt", routine.StartedAt(),
)
}

func (e exampleRoutineObserver) RunningRoutineCount(count int) {
// nothing to do in this example
}

func (e exampleRoutineObserver) RunningRoutineByNameCount(name string, count int) {
// nothing to do in this example
}
13 changes: 13 additions & 0 deletions examples/routine_leak/example2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Routine Leak Detection with AsyncRoutineManager
This demonstration illustrates how the `AsyncRoutineManager` help identifying routine leaks and pinpoint their source.

Each folder, named step1 to stepN, adds a progressive integration of the `AsyncRoutineManager`, starting from the
naked code of `step1` to the full integration in the last step.

The application we are going to use for this demonstration is very simple:

1. The `main` function repeatedly starts cycles where it processes multiple websites.
For each cycle, randomly selects 10 website URLs from a predefined list.
For each selected URL, asynchronously invokes the `getWebsiteResponseSize`
2. The `getWebsiteResponseSize` asynchronously calls the `getResponseSize` and do some fun stuff with the result
3. The `getResponseSize` contacts the site and returns the response size
69 changes: 69 additions & 0 deletions examples/routine_leak/example2/data/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package data

var Websites = []string{
"https://google.com",
"https://facebook.com",
"https://youtube.com",
"https://amazon.com",
"https://wikipedia.com",
"https://instagram.com",
"https://linkedin.com",
"https://reddit.com",
"https://ebay.com",
"https://microsoft.com",
"https://apple.com",
"https://walmart.com",
"https://espn-bad.com",
"https://bbc.com",
"https://cnn.com",
"https://foxnews.com",
"https://nytimes.com",
"https://forbes.com",
"https://cnbc.com",
"https://theguardian.com",
"https://nbcnews.com",
"https://abc.com",
"https://time.com",
"https://nationalgeographic.com",
"https://wired-bad.com",
"https://techcrunch.com",
"https://engadget.com",
"https://gizmodo.com",
"https://mashable.com",
"https://stackoverflow.com",
"https://github.com",
"https://medium-bad.com",
"https://dropbox.com",
"https://box.com",
"https://slack.com",
"https://zoom.com",
"https://stripe.com",
"https://airbnb.com",
"https://booking.com",
"https://hotels-bad.com",
"https://kayak.com",
"https://monster.com",
"https://edx.com",
"https://duolingo-bad.com",
"https://britannica-bad.com",
"https://dictionary.com",
"https://merriam-webster.com",
"https://thesaurus.com",
"https://weather-bad.com",
"https://trulia-bad.com",
"https://epicurious.com",
"https://tasteofhome.com",
"https://healthline.com",
"https://webmd.com",
"https://mayoclinic-bad.com",
"https://everydayhealth.com",
"https://livestrong.com",
"https://fool-bad.com",
"https://seekingalpha.com",
"https://nerdwallet.com",
"https://bankrate.com",
"https://creditkarma.com",
"https://mint.com",
"https://fidelity.com",
"https://vanguard.com",
}
24 changes: 24 additions & 0 deletions examples/routine_leak/example2/step1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# STEP1 - the leaking code

In this step, we will use the native `go` keyword to start go routines.
The code is very simple:

1. The `main` function repeatedly starts cycles where it processes multiple websites.
For each cycle, randomly selects 10 website URLs from a predefined list.
For each selected URL, asynchronously invokes the `getWebsiteResponseSize`
2. The `getWebsiteResponseSize` asynchronously calls the `getResponseSize` and do some fun stuff with the result
3. The `getResponseSize` contacts the site and returns the response size

Running the application we can see how the total number of goroutine keeps increasing:
```
goroutine profile: total 18
14 @ 0x102d71768 0x102d361a8 0x102d70a10 0x102db7a38 0x102db830c 0x102db82fd 0x102e451d8 0x102e4d974 0x102e87430 0x102dcd820 0x102e87610 0x102e84dac 0x102e8ad44 0x102e8ad45 0x102eb83b4 0x102d9d940 0x102ee5b18 0x102ee5aed 0x102ee6148 0x102ef6b50 0x102ef60c8 0x102d79aa4
# 0x102d70a0f internal/poll.runtime_pollWait+0x9f
...
...
goroutine profile: total 33
26 @ 0x102d71768 0x102d361a8 0x102d70a10 0x102db7a38 0x102db830c 0x102db82fd 0x102e451d8 0x102e4d974 0x102e87430 0x102dcd820 0x102e87610 0x102e84dac 0x102e8ad44 0x102e8ad45 0x102eb83b4 0x102d9d940 0x102ee5b18 0x102ee5aed 0x102ee6148 0x102ef6b50 0x102ef60c8 0x102d79aa4
# 0x102d70a0f internal/poll.runtime_pollWait+0x9f
```

Understanding which routine we are leaking is not immediate.
71 changes: 71 additions & 0 deletions examples/routine_leak/example2/step1/app_leaking_routine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"fmt"
"github.com/openshift-online/async-routine/examples/routine_leak/example2/data"
"io"
"math/rand"
"net/http"
"os"
"runtime/pprof"
"strconv"
"time"
)

func main() {
for {
for i := 0; i < 10; i++ {
url := data.Websites[rand.Intn(len(data.Websites))]
go doJob(url)
time.Sleep(500 * time.Millisecond)
}
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
}
}

func doJob(url string) {
resultChan := make(chan int64)
go getResponseSize(url, resultChan)
size := <-resultChan
// do something fun with the size - here we just avoid the compilation error
size = size
}

// getResponseSize fetches the given URL and sends the response size (in bytes) to the provided channel.
// Returns an error if the site does not exist or the request fails.
func getResponseSize(url string, ch chan<- int64) error {
// Perform the HTTP request
res, err := http.Get(url)
if err != nil {
return fmt.Errorf("site unreachable: %w", err)
}
defer res.Body.Close()

// Handle HTTP errors
if res.StatusCode != http.StatusOK {
switch res.StatusCode {
case http.StatusNotFound:
return fmt.Errorf("site does not exist (404 Not Found)")
default:
return fmt.Errorf("invalid HTTP response: %d %s", res.StatusCode, http.StatusText(res.StatusCode))
}
}

// Try to use Content-Length header if available
if contentLength := res.Header.Get("Content-Length"); contentLength != "" {
size, err := strconv.ParseInt(contentLength, 10, 64)
if err == nil && size > 0 {
ch <- size
return nil
}
}

// Calculate the size by reading the response body
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

ch <- int64(len(body))
return nil
}
7 changes: 7 additions & 0 deletions examples/routine_leak/example2/step2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# STEP2 - integrating the AsyncRoutineManager

In this step, we integrate the `AsyncRoutineManager` to handle routine management.
In fact, incorporating the `AsyncRoutineManager` is almost as simple as replacing the `go` keyword with a call
to the `NewAsyncRoutine` function.
Running this code produces the same behavior as in STEP 1, but now we have established the foundation to
fully leverage the capabilities of the `AsyncRoutineManager`.
Loading