-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Artyom Suharev
committed
Nov 25, 2024
0 parents
commit cab8460
Showing
11 changed files
with
902 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# ide | ||
.idea | ||
.vscode | ||
|
||
# local | ||
/vendor |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# Runtime Library | ||
|
||
The `runtime` library provides context-based goroutine management and signal handling for Go programs. It is designed to facilitate graceful shutdowns and handle specific signals like `SIGPIPE` when running as a systemd service. | ||
|
||
## Features | ||
|
||
- **Context-based Goroutine Management**: Manage goroutines with a base context that can be canceled, ensuring all goroutines are properly cleaned up. | ||
- **Signal Handling**: Handle `SIGPIPE` signals to prevent program crashes when running as a systemd service. | ||
- **Graceful Shutdown**: Provides mechanisms to gracefully stop and cancel running goroutines. | ||
|
||
## Installation | ||
|
||
To install the `runtime` library, add it to your `go.mod` file: | ||
|
||
```sh | ||
go get github.com/yourusername/runtime | ||
``` | ||
|
||
## Usage | ||
Example of using the `runtime` for gracefully shutting down goroutines with a default `Environment` | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"github.com/imunhatep/runtime" | ||
"log" | ||
) | ||
|
||
func main() { | ||
// If a goroutine started by Go returns non-nil error, | ||
// the framework calls env.Cancel(err) to signal other | ||
// goroutines to stop soon. | ||
runtime.Go(func(ctx context.Context) error { | ||
// Simulate work | ||
<-ctx.Done() | ||
log.Println("Goroutine stopped") | ||
return nil | ||
}) | ||
|
||
// Stop declares no more Go is called. | ||
// This is optional if env.Cancel will be called | ||
// at some point (or by a signal). | ||
runtime.Stop() | ||
|
||
// Wait returns when all goroutines return. | ||
runtime.Wait() | ||
} | ||
``` | ||
|
||
### Creating an Environment | ||
Create a new `Environment` to manage goroutines: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"github.com/yourusername/runtime" | ||
) | ||
|
||
func main() { | ||
env := runtime.NewEnvironment(context.Background()) | ||
// Use the environment to manage goroutines | ||
} | ||
``` | ||
|
||
### Starting Goroutines | ||
|
||
Use the `Go` method to start a goroutine within the environment: | ||
|
||
```go | ||
env.Go(func(ctx context.Context) error { | ||
// Your goroutine logic here | ||
<-ctx.Done() // Watch for cancellation | ||
return nil | ||
}) | ||
``` | ||
|
||
### Graceful Shutdown | ||
|
||
To gracefully stop all goroutines, call the `Stop` or `Cancel` methods: | ||
|
||
```go | ||
// Stop the environment (no new goroutines will be started) | ||
env.Stop() | ||
|
||
// Cancel the environment with an error | ||
env.Cancel(nil) | ||
|
||
// Wait for all goroutines to finish | ||
err := env.Wait() | ||
if err != nil { | ||
// Handle the error | ||
} | ||
``` | ||
|
||
|
||
### Example with environment | ||
|
||
Here is a complete example demonstrating the usage of the `runtime` library: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"github.com/imunhatep/runtime" | ||
"log" | ||
) | ||
|
||
func main() { | ||
env := runtime.NewEnvironment(context.Background()) | ||
|
||
env.Go(func(ctx context.Context) error { | ||
// Simulate work | ||
<-ctx.Done() | ||
log.Println("Goroutine stopped") | ||
return nil | ||
}) | ||
|
||
// Simulate a signal to stop the environment | ||
env.Stop() | ||
|
||
// Wait for all goroutines to finish | ||
if err := env.Wait(); err != nil { | ||
log.Fatalf("Error: %v", err) | ||
} | ||
|
||
log.Println("All goroutines have finished") | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package runtime | ||
|
||
import ( | ||
"context" | ||
"github.com/google/uuid" | ||
"github.com/rs/zerolog/log" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// Environment implements context-based goroutine management. | ||
type Environment struct { | ||
ctx context.Context | ||
cancel context.CancelFunc | ||
wg sync.WaitGroup | ||
|
||
mu sync.RWMutex | ||
stopped bool | ||
stopCh chan struct{} | ||
canceled bool | ||
err error | ||
} | ||
|
||
// NewEnvironment creates a new Environment. | ||
// | ||
// This does *not* install signal handlers for SIGINT/SIGTERM | ||
// for new environments. Only the global environment will be | ||
// canceled on these signals. | ||
func NewEnvironment(ctx context.Context) *Environment { | ||
ctx, cancel := context.WithCancel(ctx) | ||
e := &Environment{ | ||
ctx: ctx, | ||
cancel: cancel, | ||
stopCh: make(chan struct{}), | ||
} | ||
return e | ||
} | ||
|
||
// Stop just declares no further Go will be called. | ||
// | ||
// Calling Stop is optional if and only if Cancel is guaranteed | ||
// to be called at some point. For instance, if the program runs | ||
// until SIGINT or SIGTERM, Stop is optional. | ||
func (e *Environment) Stop() { | ||
e.mu.Lock() | ||
|
||
if !e.stopped { | ||
e.stopped = true | ||
close(e.stopCh) | ||
} | ||
|
||
e.mu.Unlock() | ||
} | ||
|
||
// Cancel cancels the base context. | ||
// | ||
// Passed err will be returned by Wait(). | ||
// Once canceled, Go() will not start new goroutines. | ||
// | ||
// Note that calling Cancel(nil) is perfectly valid. | ||
// Unlike Stop(), Cancel(nil) cancels the base context and can | ||
// gracefully stop goroutines started by Server.Serve or | ||
// HTTPServer.ListenAndServe. | ||
// | ||
// This returns true if the caller is the first that calls Cancel. | ||
// For second and later calls, Cancel does nothing and returns false. | ||
func (e *Environment) Cancel(err error) bool { | ||
e.mu.Lock() | ||
defer e.mu.Unlock() | ||
|
||
if e.canceled { | ||
return false | ||
} | ||
e.canceled = true | ||
e.err = err | ||
e.cancel() | ||
|
||
if e.stopped { | ||
return true | ||
} | ||
|
||
e.stopped = true | ||
close(e.stopCh) | ||
return true | ||
} | ||
|
||
// Wait waits for Stop or Cancel, and for all goroutines started by | ||
// Go to finish. | ||
// | ||
// The returned err is the one passed to Cancel, or nil. | ||
// err can be tested by IsSignaled to determine whether the | ||
// program got SIGINT or SIGTERM. | ||
func (e *Environment) Wait() error { | ||
<-e.stopCh | ||
|
||
time.Sleep(time.Second) | ||
log.Debug().Msg("[runtime] waiting for all goroutines to complete") | ||
|
||
e.wg.Wait() | ||
e.cancel() // in case no one calls Cancel | ||
|
||
e.mu.Lock() | ||
defer e.mu.Unlock() | ||
|
||
return e.err | ||
} | ||
|
||
// Go starts a goroutine that executes f. | ||
// | ||
// f takes a drived context from the base context. The context | ||
// will be canceled when f returns. | ||
// | ||
// Goroutines started by this function will be waited for by | ||
// Wait until all such goroutines return. | ||
// | ||
// If f returns non-nil error, Cancel is called immediately | ||
// with that error. | ||
// | ||
// f should watch ctx.Done() channel and return quickly when the | ||
// channel is closed. | ||
func (e *Environment) Go(f func(ctx context.Context) error) { | ||
e.mu.RLock() | ||
if e.stopped { | ||
e.mu.RUnlock() | ||
return | ||
} | ||
e.wg.Add(1) | ||
e.mu.RUnlock() | ||
|
||
go func() { | ||
ctx, cancel := context.WithCancel(e.ctx) | ||
defer cancel() | ||
err := f(ctx) | ||
if err != nil { | ||
e.Cancel(err) | ||
} | ||
e.wg.Done() | ||
}() | ||
} | ||
|
||
// GoWithID calls Go with a context having a new request tracking ID. | ||
func (e *Environment) GoWithID(f func(ctx context.Context) error) { | ||
e.Go(func(ctx context.Context) error { | ||
return f(WithRequestID(ctx, uuid.New().String())) | ||
}) | ||
} |
Oops, something went wrong.