runnable

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 17 Imported by: 16

README

Runnable

Build Status Go Reference Go Report Card

A zero-dependency Go library for orchestrating long-running processes with clean shutdown. Everything builds on a single interface:

type Runnable interface {
    Run(context.Context) error
}

Shutdown is driven by context cancellation. When the context is cancelled, a runnable stops and returns either nil or ctx.Err(), both are a clean stop. Any other error, including context.DeadlineExceeded, is a failure.

Manager

The Manager orchestrates multiple runnables with ordered shutdown. Runnables are organized in two tiers:

  • Processes — the primary work (HTTP servers, workers, scheduled tasks)
  • Services — infrastructure that processes depend on (databases, queues, metrics)

When shutdown is triggered (context cancelled or any runnable completes), processes are stopped first, then services. This ensures services remain available while processes drain.

func main() {
    m := runnable.NewManager()
    m.RegisterService(jobQueue)
    m.RegisterProcess(runnable.HTTPServer(server))
    m.RegisterProcess(monitor)

    runnable.Run(m)
}

A Manager is itself a Runnable, so managers can be nested for independent shutdown ordering.

Shutdown budget

Each shutdown phase has its own timeout: ProcessShutdownTimeout (default 15s) starts with the shutdown, ServiceShutdownTimeout (default 10s) starts when services are cancelled. Runnables still running when their phase ends are reported with ErrShutdownTimeout.

The worst case is the sum, 25s by default. Keep it below the platform grace period, such as Kubernetes terminationGracePeriodSeconds (default 30s), to leave room for the process to exit. For example, with a 60s grace period:

m := runnable.NewManager(
    runnable.ProcessShutdownTimeout(45*time.Second),
    runnable.ServiceShutdownTimeout(10*time.Second),
)

For nested managers, the inner sum must stay below the timeout of the phase the inner manager runs in. HTTPServer drains for 5s by default, which must stay below ProcessShutdownTimeout.

Names

Each runnable in a manager runs with its full name in the context, such as manager/restart/JobQueue, used in log lines and errors. Read it with NameFromContext(ctx), and set it with Named("api", r).

Example logs

Output of the package example in example_test.go: a job queue service, a scheduled cleanup task, and an app process whose completion shuts the manager down.

level=INFO msg="manager/JobQueue: started"
level=INFO msg="manager/schedule/CleanupTask: started"
level=INFO msg="manager/app: started"
JobQueue: cleanup-1
JobQueue: cleanup-2
JobQueue: cleanup-3
level=INFO msg="manager/app: stopped"
level=INFO msg="manager: starting shutdown" reason="app completed"
level=INFO msg="manager/schedule/CleanupTask: stopped"
level=INFO msg="manager/JobQueue: stopped"
level=INFO msg="manager: shutdown complete"

Entrypoints

Run, RunFunc, and RunGroup are intended as main() helpers. They handle OS signals (SIGINT/SIGTERM) and on error, log it and exit with status 1.

func main() {
    runnable.Run(myApp)
}

Wrappers

Wrappers compose behavior around a Runnable:

Wrapper Description
HTTPServer(server, opts...) Start and gracefully shut down a *http.Server
GRPCServer(addr, server, opts...) Start and gracefully stop a *grpc.Server
Restart(r, opts...) Auto-restart on exit and on failure, with configurable limits and backoff
Retry(r, opts...) Run again after errors until it succeeds, with configurable limit and backoff
Schedule(r, spec, opts...) Run on a schedule: intervals, hourly, daily, cron, or custom
Recover(r) Catch panics and return them as errors
Timeout(d, r) Cancel a runnable that runs for too long
Delay(d, r) Wait before starting a runnable
Signal(r, signals...) Cancel context on OS signals
Closer(c) Call Close() on context cancellation, also CloserErr, CloserCtx, CloserCtxErr
Func(fn) Adapt a func(context.Context) error to Runnable
Named(name, r) Give a runnable a name, readable with NameFromContext

License

The MIT License (MIT)

Documentation

Overview

Package runnable manages the lifecycle of long-running processes.

Everything is a Runnable: a Run method that blocks until its context is cancelled or its work is done. Shutdown is driven by context cancellation.

Manager runs several runnables together and stops them in order: processes first, then the services they depend on. The entry points Run, RunFunc and RunGroup add signal handling for main.

Wrappers compose behavior around a runnable: HTTPServer, GRPCServer, Restart, Retry, Schedule, Timeout, Delay, Recover, Signal, Closer, Func and Named.

Names flow through the context. A parent Manager or Named assigns them, wrappers pass them through, and NameFromContext reads them.

Example

The README "Example logs" block copies the output of this example, keep them in sync.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// JobQueue is a long-running service that processes background jobs.
type JobQueue struct{}

func (q *JobQueue) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func (q *JobQueue) Enqueue(job string) {
	fmt.Println("JobQueue: " + job)
}

// CleanupTask enqueues a cleanup job on each execution.
type CleanupTask struct {
	jobs *JobQueue
	runs int
	done chan struct{}
}

func (t *CleanupTask) Run(_ context.Context) error {
	t.runs++
	t.jobs.Enqueue(fmt.Sprintf("cleanup-%d", t.runs))
	if t.runs >= 3 {
		close(t.done)
	}
	return nil
}

// The README "Example logs" block copies the output of this example, keep them in sync.
func main() {
	jobs := &JobQueue{}
	done := make(chan struct{})
	cleanup := &CleanupTask{jobs: jobs, done: done}

	m := runnable.NewManager()
	m.RegisterService(jobs)
	m.RegisterProcess(runnable.Schedule(cleanup, runnable.Every(500*time.Millisecond)))
	m.RegisterProcess(runnable.Named("app", runnable.Func(func(_ context.Context) error {
		<-done
		return nil
	})))

	runnable.Run(m)

}
Output:
level=INFO msg="manager/JobQueue: started"
level=INFO msg="manager/schedule/CleanupTask: started"
level=INFO msg="manager/app: started"
JobQueue: cleanup-1
JobQueue: cleanup-2
JobQueue: cleanup-3
level=INFO msg="manager/app: stopped"
level=INFO msg="manager: starting shutdown" reason="app completed"
level=INFO msg="manager/schedule/CleanupTask: stopped"
level=INFO msg="manager/JobQueue: stopped"
level=INFO msg="manager: shutdown complete"

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrShutdownTimeout = errors.New("still running after shutdown timeout")

ErrShutdownTimeout is reported by Manager for each runnable still running when the timeout of its shutdown phase expires.

Functions

func ExponentialBackoff added in v1.2.0

func ExponentialBackoff(base, maxDelay time.Duration) func(errors int) time.Duration

ExponentialBackoff returns a backoff for ErrorBackoff that doubles the delay with each consecutive error, from base up to maxDelay: base, 2·base, 4·base, and so on.

For example, with a base of 1s and a max of 1m, the delays for the 1st to 8th consecutive errors are 1s, 2s, 4s, 8s, 16s, 32s, 1m and 1m:

runnable.ErrorBackoff(runnable.ExponentialBackoff(time.Second, time.Minute))
Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	backoff := runnable.ExponentialBackoff(time.Second, 10*time.Second)

	for errors := range 5 {
		fmt.Println(backoff(errors + 1))
	}

}
Output:
1s
2s
4s
8s
10s

func NameFromContext added in v1.1.0

func NameFromContext(ctx context.Context) string

NameFromContext returns the full name assigned to the running runnable by its parents, such as "manager/restart/JobQueue". Only Manager and Named assign a name, other wrappers pass it through. Returns an empty string when unset.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	var name string
	job := runnable.Func(func(ctx context.Context) error {
		name = runnable.NameFromContext(ctx)
		return nil
	})

	m := runnable.NewManager()
	m.RegisterProcess(runnable.Named("job", job))

	_ = runnable.Named("app", m).Run(context.Background())
	fmt.Println(name)

}
Output:
level=INFO msg="app/job: started"
level=INFO msg="app/job: stopped"
level=INFO msg="app: starting shutdown" reason="job completed"
level=INFO msg="app: shutdown complete"
app/job

func Run added in v0.4.0

func Run(runner Runnable)

Run runs a runnable until it returns or the process receives SIGINT or SIGTERM. On any error other than context.Canceled, it logs the error with the logger set by SetLogger and exits the process with status 1, without running deferred functions. It is intended as a main helper.

Example
package main

import (
	"context"

	"github.com/pior/runnable"
)

// Mailer is a service that sends queued emails.
type Mailer struct{}

func (*Mailer) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	// Run is meant for main: it stops on SIGINT or SIGTERM and exits the process
	// with status 1 on error, after logging it.
	m := runnable.NewManager()
	m.RegisterService(&Mailer{})
	m.RegisterProcess(runnable.Named("app", runnable.Func(func(context.Context) error {
		return nil
	})))

	runnable.Run(m)

}
Output:
level=INFO msg="manager/Mailer: started"
level=INFO msg="manager/app: started"
level=INFO msg="manager/app: stopped"
level=INFO msg="manager: starting shutdown" reason="app completed"
level=INFO msg="manager/Mailer: stopped"
level=INFO msg="manager: shutdown complete"

func RunFunc added in v0.5.0

func RunFunc(fn RunnableFunc)

RunFunc is Run for a function.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	runnable.RunFunc(func(context.Context) error {
		fmt.Println("hello")
		return nil
	})

}
Output:
hello

func RunGroup added in v0.1.0

func RunGroup(runners ...Runnable)

RunGroup runs the runnables as processes of a Manager under Run.

Example
package main

import (
	"context"

	"github.com/pior/runnable"
)

func main() {
	// RunGroup runs the runnables as processes of a manager, under Run.
	runnable.RunGroup(
		runnable.Named("migrate", runnable.Func(func(context.Context) error { return nil })),
	)

}
Output:
level=INFO msg="manager/migrate: started"
level=INFO msg="manager/migrate: stopped"
level=INFO msg="manager: starting shutdown" reason="migrate completed"
level=INFO msg="manager: shutdown complete"

func SetLogger added in v0.9.0

func SetLogger(l *slog.Logger)

SetLogger replaces the default logger with a *slog.Logger. Passing nil resets to slog.Default.

Types

type ErrorOption added in v1.1.0

type ErrorOption interface {
	RestartOption
	RetryOption
}

ErrorOption configures how Restart and Retry handle errors.

func ErrorBackoff added in v1.1.0

func ErrorBackoff(fn func(errors int) time.Duration) ErrorOption

ErrorBackoff sets the function that determines the delay before running again after an error. It receives the current consecutive error count (starting at 1). The default backs off: immediate for the first 3 errors, 10s up to 10, then 1m.

func ErrorLimit added in v1.1.0

func ErrorLimit(n int) ErrorOption

ErrorLimit sets the maximum number of consecutive errors. When reached, the last error is returned. Zero means unlimited (the default).

func ErrorResetAfter added in v1.1.0

func ErrorResetAfter(d time.Duration) ErrorOption

ErrorResetAfter resets the consecutive error count when a single run lasted at least the given duration before failing. This prevents a long-running runnable that occasionally fails from accumulating stale errors into the backoff and the limit. Zero means never reset based on duration (the default).

func OnError added in v1.1.1

func OnError(fn func(err error)) ErrorOption

OnError sets a function called with each error of the runnable, including the last one when ErrorLimit is reached. A recovered panic is a *PanicError. It is not called when the run stops on context cancellation.

type Manager added in v0.4.0

type Manager struct {
	// contains filtered or unexported fields
}

Manager coordinates the lifecycle of multiple runnables. Build it with NewManager.

Runnables are organized in two tiers: processes (foreground work) and services (infrastructure like databases or queues). Shutdown is triggered when the context is cancelled or any runnable completes. During shutdown, processes are cancelled first, then services, ensuring services remain available while processes drain.

Each runnable is wrapped with Recover to catch panics. Errors from runnables are collected, except context.Canceled which is ignored. Run returns them joined with errors.Join, each wrapped with the runnable name, so they can be inspected with errors.Is and errors.As. A runnable still running when its phase timeout expires is reported with ErrShutdownTimeout. A manager is itself a Runnable, so managers can be nested for independent shutdown ordering.

Each runnable runs with its full name in the context, such as "manager/JobQueue", see NameFromContext. A nested manager uses its assigned name as prefix, and leaves the error prefix to its parent.

Registering the same runnable twice, or as both a process and a service, panics.

func NewManager added in v0.11.0

func NewManager(opts ...ManagerOption) *Manager

NewManager returns a new Manager. Its name, used as a prefix in log messages and errors, is "manager" unless a parent Manager or Named assigns one.

The shutdown phases are bounded by ProcessShutdownTimeout and ServiceShutdownTimeout. The worst case shutdown is their sum, 25 seconds by default. It must stay below a platform grace period such as Kubernetes terminationGracePeriodSeconds, 30 seconds by default, to leave room for the process to exit. For nested managers, the inner sum must stay below the timeout of the phase the inner manager is registered in.

runnable.NewManager(runnable.ProcessShutdownTimeout(40*time.Second))
Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

// Database is a service that the processes depend on.
type Database struct{}

func (*Database) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	app := runnable.Named("app", runnable.Func(func(context.Context) error {
		return nil // the app completes, which shuts the manager down
	}))

	m := runnable.NewManager()
	m.RegisterService(&Database{})
	m.RegisterProcess(app)

	fmt.Println(m.Run(context.Background()))

}
Output:
level=INFO msg="manager/Database: started"
level=INFO msg="manager/app: started"
level=INFO msg="manager/app: stopped"
level=INFO msg="manager: starting shutdown" reason="app completed"
level=INFO msg="manager/Database: stopped"
level=INFO msg="manager: shutdown complete"
<nil>
Example (Failure)
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

// Database is a service that the processes depend on.
type Database struct{}

func (*Database) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	worker := runnable.Named("worker", runnable.Func(func(context.Context) error {
		return errors.New("connection lost")
	}))

	m := runnable.NewManager()
	m.RegisterService(&Database{})
	m.RegisterProcess(worker)

	fmt.Println(m.Run(context.Background()))

}
Output:
level=INFO msg="manager/Database: started"
level=INFO msg="manager/worker: started"
level=INFO msg="manager/worker: stopped with error" error="connection lost"
level=INFO msg="manager: starting shutdown" reason="worker died"
level=INFO msg="manager/Database: stopped"
level=INFO msg="manager: shutdown complete"
manager: worker: connection lost
Example (Nested)
package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/pior/runnable"
)

func main() {
	// Both managers log concurrently, keep the logs out of the example output.
	runnable.SetLogger(slog.New(slog.DiscardHandler))
	defer runnable.SetLogger(nil)

	// The inner manager stops its own process before its own service, while the
	// outer manager keeps its service running until the inner manager stopped.
	inner := runnable.NewManager()
	inner.RegisterService(runnable.Named("cache", runnable.Noop()))
	inner.RegisterProcess(runnable.Named("api", runnable.Noop()))

	outer := runnable.NewManager()
	outer.RegisterService(runnable.Named("database", runnable.Noop()))
	outer.RegisterProcess(runnable.Named("web", inner))

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	fmt.Println(outer.Run(ctx))

}
Output:
<nil>
Example (Options)
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// Database is a service that the processes depend on.
type Database struct{}

func (*Database) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	// stuck ignores cancellation, like a process blocked on a call without a context.
	unblock := make(chan struct{})
	defer close(unblock)
	stuck := runnable.Named("stuck", runnable.Func(func(context.Context) error {
		<-unblock
		return nil
	}))

	m := runnable.NewManager(
		runnable.ProcessShutdownTimeout(100*time.Millisecond),
		runnable.ServiceShutdownTimeout(100*time.Millisecond),
	)
	m.RegisterService(&Database{})
	m.RegisterProcess(stuck)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	err := m.Run(ctx)
	fmt.Println(err)
	fmt.Println(errors.Is(err, runnable.ErrShutdownTimeout))

}
Output:
level=INFO msg="manager/Database: started"
level=INFO msg="manager/stuck: started"
level=INFO msg="manager: starting shutdown" reason="context cancelled"
level=INFO msg="manager/stuck: still running"
level=INFO msg="manager/Database: stopped"
level=INFO msg="manager: shutdown complete"
manager: stuck: still running after shutdown timeout
true

func (*Manager) RegisterProcess added in v1.1.0

func (m *Manager) RegisterProcess(processes ...Runnable)

RegisterProcess registers processes. Processes are the primary runnables of the application. They are cancelled first during shutdown. Panics if any runnable is already registered. Duplicate detection only applies to comparable runnables, in practice pointers.

func (*Manager) RegisterService added in v1.1.0

func (m *Manager) RegisterService(services ...Runnable)

RegisterService registers services. Services are infrastructure runnables (databases, queues, etc.) that processes depend on. They are cancelled after all processes have stopped. Panics if any runnable is already registered. Duplicate detection only applies to comparable runnables, in practice pointers.

func (*Manager) Run added in v1.1.0

func (m *Manager) Run(ctx context.Context) error

Run starts all services and processes, waits for the context to be cancelled or for any runnable to return, then shuts down processes and services in turn. It returns nil when every runnable stopped cleanly, otherwise the joined errors of the runnables that failed or hung.

type ManagerOption added in v0.11.0

type ManagerOption interface {
	// contains filtered or unexported methods
}

ManagerOption configures NewManager.

func ProcessShutdownTimeout added in v1.1.0

func ProcessShutdownTimeout(d time.Duration) ManagerOption

ProcessShutdownTimeout sets how long processes have to stop, from the start of the shutdown. Defaults to 15 seconds.

func ServiceShutdownTimeout added in v1.1.0

func ServiceShutdownTimeout(d time.Duration) ManagerOption

ServiceShutdownTimeout sets how long services have to stop, from when they are cancelled after the processes stopped. Defaults to 10 seconds.

type PanicError

type PanicError struct {
	// Value is the value passed to panic.
	Value any
	// Stack is the goroutine stack trace captured when the panic was recovered.
	Stack []byte
}

PanicError is returned by Recover when the wrapped runnable panics.

func (*PanicError) Error

func (e *PanicError) Error() string

func (*PanicError) Format added in v1.1.0

func (e *PanicError) Format(f fmt.State, verb rune)

Format implements fmt.Formatter: %+v prints the error followed by the stack trace, %v and %s print the error, %q prints the error quoted.

func (*PanicError) Unwrap

func (e *PanicError) Unwrap() error

type RestartOption added in v0.6.0

type RestartOption interface {
	// contains filtered or unexported methods
}

RestartOption configures Restart.

func RestartDelay added in v0.6.0

func RestartDelay(d time.Duration) RestartOption

RestartDelay sets the time to wait before restarting after a successful exit. Defaults to zero (immediate restart).

func RestartLimit added in v0.6.0

func RestartLimit(n int) RestartOption

RestartLimit sets the maximum number of restarts after successful (nil) exits. When reached, Restart returns nil. Zero means unlimited (the default).

type RetryOption added in v1.1.0

type RetryOption interface {
	// contains filtered or unexported methods
}

RetryOption configures Retry.

type Runnable

type Runnable interface {
	Run(context.Context) error
}

Runnable is the contract for anything that runs with a Go context and expects the caller to handle errors.

Cancellation contract: when the context is cancelled, Run must stop and return either nil or ctx.Err(), which is context.Canceled under Manager and Run. Either is fine, both are a clean stop. Any other error, including context.DeadlineExceeded, is a failure.

func Closer added in v0.4.0

func Closer(c interface{ Close() }) Runnable

Closer returns a runnable that calls Close on context cancellation. There is no timeout on the call to Close.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

type Pool struct{}

func (Pool) Close() { fmt.Println("pool closed") }

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	// Close is called when the context is cancelled.
	fmt.Println(runnable.Closer(Pool{}).Run(ctx))

}
Output:
pool closed
<nil>
Example (Manager)
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

type Pool struct{}

func (Pool) Close() { fmt.Println("pool closed") }

func main() {
	// As a service, the pool is closed after all processes stopped.
	m := runnable.NewManager()
	m.RegisterService(runnable.Closer(Pool{}))
	m.RegisterProcess(runnable.Named("app", runnable.Func(func(context.Context) error {
		return nil
	})))

	fmt.Println(m.Run(context.Background()))

}
Output:
level=INFO msg="manager/closer/Pool: started"
level=INFO msg="manager/app: started"
level=INFO msg="manager/app: stopped"
level=INFO msg="manager: starting shutdown" reason="app completed"
pool closed
level=INFO msg="manager/closer/Pool: stopped"
level=INFO msg="manager: shutdown complete"
<nil>

func CloserCtx added in v0.12.0

func CloserCtx(c interface{ Close(context.Context) }) Runnable

CloserCtx returns a runnable that calls Close on context cancellation. The context passed to Close is not cancelled, so Close can perform graceful cleanup. There is no timeout on the call to Close.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

type Tracer struct{}

func (Tracer) Close(context.Context) { fmt.Println("tracer flushed") }

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	// Close receives a context that is not cancelled, to finish its cleanup.
	fmt.Println(runnable.CloserCtx(Tracer{}).Run(ctx))

}
Output:
tracer flushed
<nil>

func CloserCtxErr added in v0.12.0

func CloserCtxErr(c interface{ Close(context.Context) error }) Runnable

CloserCtxErr returns a runnable that calls Close on context cancellation. The context passed to Close is not cancelled, so Close can perform graceful cleanup. There is no timeout on the call to Close.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

type Client struct{}

func (Client) Close(context.Context) error {
	fmt.Println("client closed")
	return nil
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	fmt.Println(runnable.CloserCtxErr(Client{}).Run(ctx))

}
Output:
client closed
<nil>

func CloserErr added in v0.12.0

func CloserErr(c interface{ Close() error }) Runnable

CloserErr returns a runnable that calls Close on context cancellation. There is no timeout on the call to Close.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

type DB struct{ err error }

func (db DB) Close() error {
	fmt.Println("db closed")
	return db.err
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	errBusy := errors.New("connection busy")
	err := runnable.CloserErr(DB{err: errBusy}).Run(ctx)
	fmt.Println(err)
	fmt.Println(errors.Is(err, errBusy))

}
Output:
db closed
close: connection busy
true

func Delay added in v1.1.0

func Delay(d time.Duration, r Runnable) Runnable

Delay returns a runnable that waits for d before running the given runnable, for example to stagger the start of jobs. Context cancellation during the wait returns the context error without running it.

runnable.Delay(30*time.Second, warmup)
Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	warmup := runnable.Named("warmup", runnable.Func(func(context.Context) error {
		fmt.Println("warming up")
		return nil
	}))

	fmt.Println(runnable.Delay(10*time.Millisecond, warmup).Run(context.Background()))

}
Output:
level=INFO msg="delay/warmup: waiting" delay=10ms
level=INFO msg="delay/warmup: starting"
warming up
<nil>

func Func added in v0.5.0

func Func(fn RunnableFunc) Runnable

Func returns a Runnable from a function. Its name is the function name, as reported by runtime.FuncForPC.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	hello := runnable.Func(func(context.Context) error {
		fmt.Println("hello")
		return nil
	})

	fmt.Println(hello.Run(context.Background()))

}
Output:
hello
<nil>
Example (Worker)
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	jobs := make(chan string)
	ctx, cancel := context.WithCancel(context.Background())

	// A worker loops until cancelled, then returns ctx.Err(): a clean stop.
	worker := runnable.Func(func(ctx context.Context) error {
		for {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case job := <-jobs:
				fmt.Println("processed", job)
			}
		}
	})

	go func() {
		jobs <- "a"
		jobs <- "b"
		cancel()
	}()

	fmt.Println(worker.Run(ctx))

}
Output:
processed a
processed b
context canceled

func GRPCServer added in v1.2.0

func GRPCServer(addr string, s interface {
	Serve(net.Listener) error
	GracefulStop()
	Stop()
}, opts ...ServerOption,
) Runnable

GRPCServer returns a runnable that runs a *grpc.Server, or any server with the same Serve, GracefulStop and Stop methods. It listens on addr unless Listener is set.

On context cancellation, it calls GracefulStop to drain in-flight calls before returning, for at most DrainTimeout, 5 seconds by default. If the drain times out, it calls Stop to close the remaining connections.

runnable.GRPCServer(":50051", grpc.NewServer(), runnable.DrainTimeout(10*time.Second))
Example
package main

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/pior/runnable"
)

// GreeterServer stands in for a *grpc.Server, to keep grpc out of the examples.
type GreeterServer struct{ stopped chan struct{} }

func (s *GreeterServer) Serve(ln net.Listener) error {
	<-s.stopped
	return ln.Close()
}

func (s *GreeterServer) GracefulStop() { close(s.stopped) }

func (s *GreeterServer) Stop() {}

func main() {
	server := &GreeterServer{stopped: make(chan struct{})} // grpc.NewServer()

	ctx, cancel := context.WithCancel(context.Background())
	time.AfterFunc(100*time.Millisecond, cancel)

	fmt.Println(runnable.GRPCServer("127.0.0.1:18081", server).Run(ctx))

}
Output:
level=INFO msg="grpcserver: listening" addr=127.0.0.1:18081
level=INFO msg="grpcserver: draining" timeout=5s
level=INFO msg="grpcserver: drained"
<nil>

func HTTPServer added in v0.3.0

func HTTPServer(s *http.Server, opts ...ServerOption) Runnable

HTTPServer returns a runnable that runs a *http.Server.

On context cancellation, it calls http.Server.Shutdown to gracefully drain in-flight requests before returning, for at most DrainTimeout, 5 seconds by default. The server listens on http.Server.Addr unless Listener is set.

runnable.HTTPServer(server, runnable.DrainTimeout(10*time.Second))
Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/pior/runnable"
)

func main() {
	server := &http.Server{
		Addr:    "127.0.0.1:18080",
		Handler: http.NotFoundHandler(),
	}

	ctx, cancel := context.WithCancel(context.Background())
	time.AfterFunc(100*time.Millisecond, cancel)

	fmt.Println(runnable.HTTPServer(server).Run(ctx))

}
Output:
level=INFO msg="httpserver: listening" addr=127.0.0.1:18080
level=INFO msg="httpserver: draining" timeout=5s
level=INFO msg="httpserver: drained"
<nil>
Example (Error)
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/pior/runnable"
)

func main() {
	server := &http.Server{
		Addr:    "INVALID",
		Handler: http.NotFoundHandler(),
	}

	fmt.Println(runnable.HTTPServer(server).Run(context.Background()))

}
Output:
listen tcp: address INVALID: missing port in address
Example (Manager)
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/pior/runnable"
)

// Cache is a service that the server depends on.
type Cache struct{}

func (*Cache) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	server := &http.Server{
		Addr:    "127.0.0.1:18080",
		Handler: http.NotFoundHandler(),
	}

	// The manager stops the server first, draining in-flight requests, then the
	// job queue it depends on.
	m := runnable.NewManager()
	m.RegisterService(&Cache{})
	m.RegisterProcess(runnable.HTTPServer(server))

	ctx, cancel := context.WithCancel(context.Background())
	time.AfterFunc(100*time.Millisecond, cancel)

	fmt.Println(m.Run(ctx))

}
Output:
level=INFO msg="manager/Cache: started"
level=INFO msg="manager/httpserver: started"
level=INFO msg="manager/httpserver: listening" addr=127.0.0.1:18080
level=INFO msg="manager: starting shutdown" reason="context cancelled"
level=INFO msg="manager/httpserver: draining" timeout=5s
level=INFO msg="manager/httpserver: drained"
level=INFO msg="manager/httpserver: stopped"
level=INFO msg="manager/Cache: stopped"
level=INFO msg="manager: shutdown complete"
<nil>
Example (Options)
package main

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"time"

	"github.com/pior/runnable"
)

func main() {
	// The listening address is random, keep it out of the example output.
	runnable.SetLogger(slog.New(slog.DiscardHandler))
	defer runnable.SetLogger(nil)

	// Listener serves on a listener opened by the caller: port 0 in tests, a unix
	// socket, a TLS listener, or a socket passed by the service manager.
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}

	server := &http.Server{
		Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			_, _ = io.WriteString(w, "hello")
		}),
	}

	r := runnable.HTTPServer(server,
		runnable.Listener(ln),
		runnable.DrainTimeout(10*time.Second),
	)

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error)
	go func() { done <- r.Run(ctx) }()

	resp, err := http.Get("http://" + ln.Addr().String())
	if err != nil {
		panic(err)
	}
	body, _ := io.ReadAll(resp.Body)
	_ = resp.Body.Close()
	fmt.Println(string(body))

	cancel()
	fmt.Println(<-done)

}
Output:
hello
<nil>

func Named added in v1.1.0

func Named(name string, r Runnable) Runnable

Named returns a runnable that runs r under the given name. The name is used by a parent Manager for its log lines and errors, and when no parent assigns one, it is passed to r through the context (see NameFromContext).

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	// A function is named after its Go symbol, Named gives it a readable name
	// for log lines and errors.
	api := runnable.Named("api", runnable.Func(func(context.Context) error {
		return errors.New("port in use")
	}))

	m := runnable.NewManager()
	m.RegisterProcess(api)

	fmt.Println(m.Run(context.Background()))

}
Output:
level=INFO msg="manager/api: started"
level=INFO msg="manager/api: stopped with error" error="port in use"
level=INFO msg="manager: starting shutdown" reason="api died"
level=INFO msg="manager: shutdown complete"
manager: api: port in use

func Noop added in v0.13.0

func Noop() Runnable

Noop returns a runnable that does nothing and returns when the context is cancelled.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	// Noop blocks until the context is cancelled.
	fmt.Println(runnable.Noop().Run(ctx))

}
Output:
context canceled
Example (ServicesOnly)
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// Metrics is a service that exports metrics.
type Metrics struct{}

func (*Metrics) Run(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func main() {
	// A manager shuts down when any runnable returns. Noop is a process that
	// never does, so a manager of services runs until it is cancelled.
	m := runnable.NewManager()
	m.RegisterService(&Metrics{})
	m.RegisterProcess(runnable.Named("idle", runnable.Noop()))

	ctx, cancel := context.WithCancel(context.Background())
	time.AfterFunc(10*time.Millisecond, cancel)

	fmt.Println(m.Run(ctx))

}
Output:
level=INFO msg="manager/Metrics: started"
level=INFO msg="manager/idle: started"
level=INFO msg="manager: starting shutdown" reason="context cancelled"
level=INFO msg="manager/idle: stopped"
level=INFO msg="manager/Metrics: stopped"
level=INFO msg="manager: shutdown complete"
<nil>

func Recover

func Recover(runnable Runnable) Runnable

Recover returns a runnable that recovers a panic in the given runnable and returns it as a *PanicError. Manager wraps every registered runnable with it, so it is only needed on runnables run directly.

Example
package main

import (
	"context"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	r := runnable.Recover(runnable.Func(func(context.Context) error {
		panic("boom")
	}))

	fmt.Println(r.Run(context.Background()))

}
Output:
runnable panicked: boom
Example (PanicError)
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	r := runnable.Recover(runnable.Func(func(context.Context) error {
		panic(errors.New("nil map"))
	}))

	err := r.Run(context.Background())

	// PanicError carries the panic value and the stack, printed by %+v.
	if pe, ok := errors.AsType[*runnable.PanicError](err); ok {
		fmt.Println("panic value:", pe.Value)
		fmt.Println("has stack:", len(pe.Stack) > 0)
	}

}
Output:
panic value: nil map
has stack: true

func Restart

func Restart(runnable Runnable, opts ...RestartOption) Runnable

Restart returns a runnable that keeps running the given runnable, restarting it after both successful exits and errors. Panics are recovered and treated as errors.

On successful exit, the runnable is restarted after RestartDelay. On error, it is restarted after the backoff of ErrorBackoff. The error count tracks consecutive errors and resets to zero after any successful run, or after a long enough run with ErrorResetAfter.

It loops indefinitely unless limited by RestartLimit or ErrorLimit. When the restart limit is reached, Run returns nil. When the error limit is reached, Run returns the last error. Errors that are restarted are logged. OnError is called with each error. Context cancellation stops the loop and returns context.Canceled.

runnable.Restart(worker, runnable.ErrorLimit(5), runnable.ErrorResetAfter(time.Minute))
Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	// By default, Restart restarts forever, after success and after errors.
	runs := 0
	worker := runnable.Named("worker", runnable.Func(func(context.Context) error {
		runs++
		if runs == 3 {
			cancel()
		}
		if runs == 2 {
			return errors.New("failed")
		}
		return nil
	}))

	fmt.Println(runnable.Restart(worker).Run(ctx))

}
Output:
level=INFO msg="restart/worker: starting" restart=0 errors=0
level=INFO msg="restart/worker: starting" restart=1 errors=0
level=INFO msg="restart/worker: failed, restarting" error=failed errors=1 delay=0s
level=INFO msg="restart/worker: starting" restart=2 errors=1
context canceled
Example (ErrorLimit)
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/pior/runnable"
)

func main() {
	connect := runnable.Named("connect", runnable.Func(func(context.Context) error {
		return errors.New("connection refused")
	}))

	fmt.Println(runnable.Restart(connect, runnable.ErrorLimit(3)).Run(context.Background()))

}
Output:
level=INFO msg="restart/connect: starting" restart=0 errors=0
level=INFO msg="restart/connect: failed, restarting" error="connection refused" errors=1 delay=0s
level=INFO msg="restart/connect: starting" restart=1 errors=1
level=INFO msg="restart/connect: failed, restarting" error="connection refused" errors=2 delay=0s
level=INFO msg="restart/connect: starting" restart=2 errors=2
level=INFO msg="restart/connect: not restarting" reason="error limit" limit=3
connection refused
Example (Options)
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	runs := 0
	worker := runnable.Named("worker", runnable.Func(func(context.Context) error {
		runs++
		if runs <= 2 {
			return errors.New("failed")
		}
		return nil
	}))

	r := runnable.Restart(worker,
		// Stop after 2 restarts following a successful run, errors do not count.
		runnable.RestartLimit(2),
		runnable.RestartDelay(10*time.Millisecond),
		// Give up after 5 consecutive errors, returning the last one.
		runnable.ErrorLimit(5),
		runnable.ErrorBackoff(func(errors int) time.Duration {
			return time.Duration(errors) * 10 * time.Millisecond
		}),
		// A run lasting a minute before failing resets the error count.
		runnable.ErrorResetAfter(time.Minute),
	)

	fmt.Println(r.Run(context.Background()))

}
Output:
level=INFO msg="restart/worker: starting" restart=0 errors=0
level=INFO msg="restart/worker: failed, restarting" error=failed errors=1 delay=10ms
level=INFO msg="restart/worker: starting" restart=1 errors=1
level=INFO msg="restart/worker: failed, restarting" error=failed errors=2 delay=20ms
level=INFO msg="restart/worker: starting" restart=2 errors=2
level=INFO msg="restart/worker: starting" restart=3 errors=0
level=INFO msg="restart/worker: starting" restart=4 errors=0
level=INFO msg="restart/worker: not restarting" reason="restart limit" limit=2
<nil>

func Retry added in v1.1.0

func Retry(runnable Runnable, opts ...RetryOption) Runnable

Retry returns a runnable that runs the given runnable again after each error, until it succeeds. Panics are recovered and treated as errors.

On error, the runnable runs again after the backoff of ErrorBackoff, and Retry logs the error. It retries indefinitely unless limited by ErrorLimit: when the limit is reached, Run returns the last error. ErrorResetAfter resets the error count after a long enough run. OnError is called with each error. Context cancellation stops the loop and returns the context error.

Unlike Restart, a successful run is not restarted: Run returns nil.

runnable.Retry(migrate, runnable.ErrorLimit(5))
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	// A migration that fails until the database is up.
	runs := 0
	migrate := runnable.Named("migrate", runnable.Func(func(context.Context) error {
		runs++
		if runs < 3 {
			return errors.New("connection refused")
		}
		fmt.Println("migrated")
		return nil
	}))

	r := runnable.Retry(migrate,
		runnable.ErrorLimit(5),
		runnable.ErrorBackoff(func(int) time.Duration { return 10 * time.Millisecond }),
	)

	fmt.Println(r.Run(context.Background()))

}
Output:
level=INFO msg="retry/migrate: failed, retrying" error="connection refused" errors=1 delay=10ms
level=INFO msg="retry/migrate: failed, retrying" error="connection refused" errors=2 delay=10ms
migrated
<nil>

func Schedule added in v1.0.0

func Schedule(runnable Runnable, spec ScheduleSpec, opts ...ScheduleOption) Runnable

Schedule returns a runnable that runs the given runnable according to spec.

If an execution outlasts the interval, missed ticks are skipped (not queued). On error from the inner runnable, Schedule stops and returns the error, unless ScheduleContinueOnError is set. On context cancellation, returns context.Canceled.

For example:

runnable.Schedule(cleanup, runnable.DailyAt(3, 0))

To run at several times, use several schedules, or a Cron schedule. For custom scheduling logic, pass a ScheduleSpec function directly, or adapt any type with a Next method using Cron. For example, to use github.com/robfig/cron/v3:

sched, _ := cron.ParseStandard("15 */6 * * *") // every 6h at :15
runnable.Schedule(worker, runnable.Cron(sched))
Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	runs := 0
	cleanup := runnable.Func(func(context.Context) error {
		runs++
		fmt.Println("cleanup", runs)
		if runs == 3 {
			cancel()
		}
		return nil
	})

	fmt.Println(runnable.Schedule(cleanup, runnable.Every(10*time.Millisecond)).Run(ctx))

}
Output:
cleanup 1
cleanup 2
cleanup 3
context canceled

func Signal

func Signal(runnable Runnable, signals ...os.Signal) Runnable

Signal returns a runnable that runs the given runnable and cancels its context when the process receives one of the signals, syscall.SIGINT and syscall.SIGTERM by default. It stops listening when Run returns, and after the first signal so that a second one terminates the process.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"syscall"

	"github.com/pior/runnable"
)

// sendSignal returns a runnable that sends sig to its own process, like a user
// pressing Ctrl-C or Kubernetes stopping a pod, then waits for the cancellation.
func sendSignal(sig os.Signal) runnable.Runnable {
	return runnable.Named("app", runnable.Func(func(ctx context.Context) error {
		p, _ := os.FindProcess(os.Getpid())
		_ = p.Signal(sig)
		<-ctx.Done()
		return ctx.Err()
	}))
}

func main() {
	// Signal cancels the context on SIGINT or SIGTERM. Run already does it.
	r := runnable.Signal(sendSignal(syscall.SIGTERM))

	fmt.Println(r.Run(context.Background()))

}
Output:
level=INFO msg="signal/app: received signal" signal=terminated
context canceled
Example (Signals)
package main

import (
	"context"
	"fmt"
	"os"
	"syscall"

	"github.com/pior/runnable"
)

// sendSignal returns a runnable that sends sig to its own process, like a user
// pressing Ctrl-C or Kubernetes stopping a pod, then waits for the cancellation.
func sendSignal(sig os.Signal) runnable.Runnable {
	return runnable.Named("app", runnable.Func(func(ctx context.Context) error {
		p, _ := os.FindProcess(os.Getpid())
		_ = p.Signal(sig)
		<-ctx.Done()
		return ctx.Err()
	}))
}

func main() {
	// Cancel the context on SIGHUP only.
	r := runnable.Signal(sendSignal(syscall.SIGHUP), syscall.SIGHUP)

	fmt.Println(r.Run(context.Background()))

}
Output:
level=INFO msg="signal/app: received signal" signal=hangup
context canceled

func Timeout added in v1.1.0

func Timeout(d time.Duration, r Runnable) Runnable

Timeout returns a runnable that cancels the given runnable when it runs for longer than d.

The result of the runnable is returned as is, so a runnable that stops cleanly with nil on the timeout succeeds. When it returns the bare context.DeadlineExceeded of the timeout, Timeout returns "timed out after <d>: context deadline exceeded" instead, which still wraps it. A cancellation of the parent context is not a timeout.

runnable.Schedule(runnable.Timeout(time.Minute, job), runnable.Every(time.Hour))
Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	// A job that is stuck, and stops when cancelled.
	job := runnable.Named("report", runnable.Func(func(ctx context.Context) error {
		<-ctx.Done()
		return ctx.Err()
	}))

	err := runnable.Timeout(100*time.Millisecond, job).Run(context.Background())
	fmt.Println(err)

}
Output:
level=INFO msg="timeout/report: timed out" timeout=100ms
timed out after 100ms: context deadline exceeded

type RunnableFunc added in v0.5.0

type RunnableFunc func(context.Context) error

RunnableFunc is a function that implements the Runnable contract.

type ScheduleOption added in v1.1.0

type ScheduleOption interface {
	// contains filtered or unexported methods
}

ScheduleOption configures Schedule.

func ScheduleContinueOnError added in v1.1.0

func ScheduleContinueOnError() ScheduleOption

ScheduleContinueOnError makes Schedule log an error returned by the runnable and run it again at the next tick, instead of stopping and returning the error.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pior/runnable"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	runs := 0
	report := runnable.Named("report", runnable.Func(func(context.Context) error {
		runs++
		if runs == 1 {
			return errors.New("database unavailable")
		}
		fmt.Println("report", runs)
		cancel()
		return nil
	}))

	// The first run fails: the error is logged, and the report runs again at the next tick.
	r := runnable.Schedule(report, runnable.Every(10*time.Millisecond), runnable.ScheduleContinueOnError())

	fmt.Println(r.Run(ctx))

}
Output:
level=INFO msg="schedule/report: failed, continuing" error="database unavailable"
report 2
context canceled

type ScheduleSpec added in v1.0.0

type ScheduleSpec func(lastStart, now time.Time) time.Time

ScheduleSpec computes the next execution time given the last execution start and the current time. Interval specs like Every use lastStart to account for execution time. Clock-aligned specs like DailyAt use now.

func Cron added in v1.1.0

func Cron(s interface{ Next(time.Time) time.Time }) ScheduleSpec

Cron returns a schedule spec that delegates to s.Next(now). It adapts any schedule exposing a Next method, such as github.com/robfig/cron/v3's cron.Schedule, without depending on it.

Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// now is a fixed time for the schedule spec examples: Friday 2026-01-02 10:15.
var now = time.Date(2026, 1, 2, 10, 15, 0, 0, time.UTC)

// weekdays fires at 09:00 on weekdays. Any type with a Next method works with
// Cron, such as the schedules of github.com/robfig/cron/v3.
type weekdays struct{}

func (weekdays) Next(now time.Time) time.Time {
	next := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, now.Location())
	for !next.After(now) || next.Weekday() == time.Saturday || next.Weekday() == time.Sunday {
		next = next.AddDate(0, 0, 1)
	}
	return next
}

func main() {
	spec := runnable.Cron(weekdays{})

	// Friday 10:15, the next weekday 09:00 is Monday.
	fmt.Println(spec(time.Time{}, now))

}
Output:
2026-01-05 09:00:00 +0000 UTC

func DailyAt added in v1.0.0

func DailyAt(hour, minute int) ScheduleSpec

DailyAt returns a schedule spec that triggers at the given hour and minute each day.

Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// now is a fixed time for the schedule spec examples: Friday 2026-01-02 10:15.
var now = time.Date(2026, 1, 2, 10, 15, 0, 0, time.UTC)

func main() {
	fmt.Println(runnable.DailyAt(18, 0)(time.Time{}, now))
	fmt.Println(runnable.DailyAt(3, 0)(time.Time{}, now))

}
Output:
2026-01-02 18:00:00 +0000 UTC
2026-01-03 03:00:00 +0000 UTC

func Every added in v0.11.0

func Every(d time.Duration) ScheduleSpec

Every returns a schedule spec that triggers at regular intervals, accounting for execution time. If the runnable takes longer than the interval, the next execution starts immediately (missed ticks are skipped, not queued).

Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// now is a fixed time for the schedule spec examples: Friday 2026-01-02 10:15.
var now = time.Date(2026, 1, 2, 10, 15, 0, 0, time.UTC)

func main() {
	spec := runnable.Every(time.Hour)

	lastStart := now.Add(-10 * time.Minute)
	fmt.Println(spec(lastStart, now))

	// A run that outlasted the interval is followed immediately.
	lastStart = now.Add(-2 * time.Hour)
	fmt.Println(spec(lastStart, now))

}
Output:
2026-01-02 11:05:00 +0000 UTC
2026-01-02 10:15:00 +0000 UTC

func Hourly added in v1.0.0

func Hourly() ScheduleSpec

Hourly returns a schedule spec that triggers at the top of every hour (:00).

Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// now is a fixed time for the schedule spec examples: Friday 2026-01-02 10:15.
var now = time.Date(2026, 1, 2, 10, 15, 0, 0, time.UTC)

func main() {
	fmt.Println(runnable.Hourly()(time.Time{}, now))

}
Output:
2026-01-02 11:00:00 +0000 UTC

func HourlyAt added in v1.0.0

func HourlyAt(minute int) ScheduleSpec

HourlyAt returns a schedule spec that triggers at the given minute past each hour.

Example
package main

import (
	"fmt"
	"time"

	"github.com/pior/runnable"
)

// now is a fixed time for the schedule spec examples: Friday 2026-01-02 10:15.
var now = time.Date(2026, 1, 2, 10, 15, 0, 0, time.UTC)

func main() {
	fmt.Println(runnable.HourlyAt(30)(time.Time{}, now))
	fmt.Println(runnable.HourlyAt(5)(time.Time{}, now))

}
Output:
2026-01-02 10:30:00 +0000 UTC
2026-01-02 11:05:00 +0000 UTC

type ServerOption added in v1.1.0

type ServerOption interface {
	// contains filtered or unexported methods
}

ServerOption configures HTTPServer and GRPCServer.

func DrainTimeout added in v1.1.0

func DrainTimeout(d time.Duration) ServerOption

DrainTimeout sets the maximum time allowed for the graceful shutdown of a HTTPServer or GRPCServer. Defaults to 5 seconds. Under a Manager, keep it below ProcessShutdownTimeout.

func Listener added in v1.1.0

func Listener(ln net.Listener) ServerOption

Listener makes a HTTPServer or GRPCServer accept connections on ln instead of listening on its address. Use it to listen on port 0 in tests, on a unix socket, on a TLS listener, or on a socket passed by the service manager (socket activation). The server takes ownership of ln and closes it on shutdown.

Directories

Path Synopsis
examples
loggers module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL