timestone

package module
v1.0.5 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2024 License: Apache-2.0 Imports: 2 Imported by: 0

README

Timestone 🗿

Timestone is a library to improve unit tests for time-dependent, concurrent go code. Existing libraries such as Quartz or Clock show the need for such a tool, yet have various shortcomings, for example not being able to reliably prevent race-conditions in tests, or being difficult to read and understand when used.

Goals

This library is built around the following primary design goals:

  • 🤌 Eliminate flaky unit tests
  • 🧹Keep unit tests free of boilerplate syncing code and focussed on assumptions and expectations
  • 🐭 As little invasive as possible to the tested implementation

Secondary goals are a good separation of concerns and extensive test coverage for the library itself.

Design principles

To offer a high-level API that keeps manual syncing code out of unit tests, Timestone takes an opinionated, use-case-oriented approach rather than attempting to substitute low-level runtime primitives like timers and tickers. However this approach can be limiting, and there may be a need to extend the library’s public interface to support additional use cases. For instance, Timestone’s public model already includes passing the commonly used context.Context but the simulation implementation doesn't alawys respect it. Another example is a cron syntax for scheduling recurring tasks which could be easily integrated but is not included yet.

Concepts

To achieve its goals, Timestone aims to encapsulate concurrency. Instead of directly invoking goroutines, the library provides a Scheduler interface with methods for scheduling Actions, such as one-time or recurring tasks. There are two implementations of the Scheduler: system.Scheduler and simulation.Scheduler. While the former uses standard library runtime primitives to dispatch actions, the latter employs a run loop to control where actions are scheduled. Through various configuration options, the scheduling mode and order of actions can be controlled, and action dependencies can be setup.

To see how this works in practice, take a look at the examples package, which contains functional test cases that serve as integration tests for the Timestone library, as well as demonstrative use cases.

The following sections provide a more detailed explanation:

Scheduler

One of the main challenges in eliminating race conditions from unit tests is handling goroutines. Non-deterministic by nature, goroutines provide no guarantee on the order in which concurrent code will be executed by the Go runtime. To address this problem in unit tests, Timestone offers a Scheduler interface designed to run code concurrently while encapsulating the underlying complexity:

type Scheduler interface {
    Clock
    PerformNow(ctx context.Context, action Action)
    PerformAfter(ctx context.Context, action Action, duration time.Duration)
    PerformRepeatedly(ctx context.Context, action Action, until *time.Time, interval time.Duration)
}

Where you would normally call go func() {...}(), when working with Timestone you instead use the PerformNow method of the Scheduler. The PerformAfter and PerformRepeatedly methods offer convenient alternatives to using time.Timer and time.Ticker within goroutines for scheduling function execution.

While the system.Scheduler implementation of the Scheduler interface uses the mentioned runtime scheduling primitives, the simulation.Scheduler implementation is where the real magic happens.

Rather than immediately running an action within a goroutine, the simulation.Scheduler creates an event generator for it. The events it produces will then be executed from either the ForwardOne or Forward methods, advancing the simulation.Scheduler’s clock either to the next event or through all events scheduled to occur within a specified time.Duration. Additional configuration can be provided for individual actions or entire groups, allowing control over the scheduling order of simultaneous events or injecting dependencies between actions, delaying the execution of certain actions until their dependencies have completed.

To provide this level of control, the simulation.Scheduler uses a run loop that iterates over all events in a well-defined order until no event remains. For each event, its configuration and default settings are considered to determine whether it should execute sequentially or asynchronously, or if it must wait on other events.

Action

An Action defines an interface for a function to be executed, along with a name used to associate it with a configuration.

type Action interface {
	Perform(ActionContext)
	Name() string
}

The ActionContext provided to the Perform method offers contextual information such as a clock and a control mechanism that supports the recursive scheduling of additional actions when using the simulation.Scheduler. You can either use the included SimpleAction as a convenient wrapper or create your own implementation.

Event

An Event is an internal concept of the simulation.Scheduler that combines an Action with a time.Time that determines when it should be executed. These events are produced from actions by event generators. For example, when calling simulation.Scheduler.PerformRepeatedly, a corresponding event generator is registered, which repeatedly places events into the event loop according to its settings.

When using the simulation.Scheduler for deterministic unit tests, you configure events by providing EventConfigurations. These configurations can target events by the name of their embedded action, or individually by their name and execution time.

Contributing

This project is in its early stages, and contributions are welcome. Feel free to fork the repository and submit a pull request (PR). When submitting a PR, it would be helpful to reference an open issue so that there is documentation for future reference.

Currently, the most important features on the agenda for this project are:

  • Pipeline for linting and automatic unit tests before merging
  • Support for canceled contexts

Reporting a Bug

To report a bug, please create an issue ticket. Include sufficient code samples and contextual information to reproduce the bug. If you can provide a fix, it will be greatly appreciated.

Documentation

Index

Constants

View Source
const ActionContextClockKey = "actionContextClock"

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action interface {
	Perform(ActionContext)
	Name() string
}

type ActionContext

type ActionContext interface {
	context.Context
	Clock() Clock
	DoneSchedulingNewActions()
}

type Clock

type Clock interface {
	Now() time.Time
}

type Scheduler

type Scheduler interface {
	Clock
	PerformNow(ctx context.Context, action Action)
	PerformAfter(ctx context.Context, action Action, duration time.Duration)
	PerformRepeatedly(ctx context.Context, action Action, until *time.Time, interval time.Duration)
}

type SimpleAction

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

func NewSimpleAction

func NewSimpleAction(action func(ActionContext), name string) *SimpleAction

func (*SimpleAction) Name

func (s *SimpleAction) Name() string

func (*SimpleAction) Perform

func (s *SimpleAction) Perform(ctx ActionContext)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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