Skip to content

Commit

Permalink
init library code
Browse files Browse the repository at this point in the history
  • Loading branch information
Artyom Suharev committed Nov 25, 2024
0 parents commit cab8460
Show file tree
Hide file tree
Showing 11 changed files with 902 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# ide
.idea
.vscode

# local
/vendor
214 changes: 214 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions README.md
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")
}
```
146 changes: 146 additions & 0 deletions env.go
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()))
})
}
Loading

0 comments on commit cab8460

Please sign in to comment.