abtime

package module
v1.0.7 Latest Latest
Warning

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

Go to latest
Published: Apr 18, 2023 License: MIT Imports: 3 Imported by: 1

README

abtime

Build Status

go get github.com/thejerf/abtime

A library for abstracting away from the literal Go time library and the context's cancellation and timeout libraries, for testing and time control.

In any code that seriously uses time, such as billing or scheduling code, best software engineering practices are that you should not directly access the operating system time. This module provides you with code to implement that principle in Go.

See some discussions:

This module is fully covered with godoc, including examples, usage, and everything else you might expect from a README.md on GitHub. (DRY.)

Most if not all other time testing abstractions for Go attempt to simulate the passage of time itself. That is, you can set a Timer for a second from now, then, you tell the time replacement module that one second has passed, and it will trigger the timer at that point.

That is indeed simpler for simple use cases than what I have here, and permits a drop-in interface replacement for the whole module. However, it does not permit you to test all scenarios, because it is built on a fundamentally false premise, which is that time is a monotonic, agreed-upon value for all goroutines. That is not how goroutines "perceive" time.

In reality, if you give one goroutine a timer for 1 second in the future, and another goroutine a timer for 1.1 seconds in the future, it is entirely possible for the second goroutine to entirely finish its execution before the first one even gets woken up. (The first goroutine may well have had its timer triggered, but then immediately descheduled for whatever reason, while the second runs to completion.)

Proper testing of complex time-dependent multi-goroutine coordination requires deeper levels of control than a compatible API can offer. This package takes the hit of having to add unique IDs to timers and tickers in order to permit a deeper level of testing, as proper testing of time-sensitive code must be able to consider the case where events in different goroutines happen "out of order", because they will.

If you only have one goroutine using time-based code then this package may be overkill. However, if you have multiple goroutines interacting with each other while also referring to the clock, you may find this package is worthwhile as it will permit you to set up important test scenarios that drop-in replacements for the time package simply can not express.

Changelog

  • 1.0.7:
    • Fixups in the internal registration of triggerable events.
    • Added wrappers around context.WithTimeout and context.WithCancel that allow controlled cancellation of contexts like the rest of time-based code.
  • 1.0.6:
    • Manual timer needs to reflect whether it was stopped, not that it was stopped.
    • This also cleans up some of the concurrency. This was one of my earlier libraries. The place where a goroutine is spawned to perform the various triggered actions should now be more correct.
  • 1.0.5:
    • (INCORRECT) The manual timer is ALWAYS successfully stopped by a Stop call, so .Stop must always return true.
  • 1.0.4:
    • Add ticker.Reset for Go 1.15. This version requires Go 1.15.
    • Add proper go module support.
  • 1.0.3:
    • Fix locking for Unregister and UnregisterAll.
  • 1.0.2
    • Adds support for unregistering triggers, so the ids can be reused with the same abtime object.

      As the godoc says, this is a sign of some sort of flaw, but it is not yet clear how to handle it. I still haven't found a good option for an API for this stuff. My original goal with abtime was to be as close to the original time API as possible, I'm considering abandoning that. Though I still don't know what exactly that would look like.

      (Plus, this need some sort of context support now.)

  • 1.0.1
    • Issue 3 reports a reversal in the sense of the timer.Reset return value, which is fixed. While fixing this, a race condition in setting the underlying value was also fixed.
  • 1.0.0
    • Initial Release.

On the Abtime API

Perusing the abtime API will quickly reveal that unlike other clock-mocking libraries in Go, the abtime API introduces additional ID parameters to the calls you make, which are ignored when using real time.

This is because as nice as the API may be, it is a mistake for a clock-mocking library to bake in the assumption that your code can assume the existence of a monotonic time, because in multi-threaded code, you can not count on that. Just because you have alarm A firing in 1000ms in one thread and alarm B firing in a different thread in 1001ms, that does not mean that you can assume A will fire before B. That's an error in multithreading code, and in my opinion, any mocking library needs to be able to help you test that even if B fires first, everything will still work as desired. Writing into the API that the only way to trigger alarms and such is by monotonically advancing the clock precludes this possibility from the beginning, however convenient it may be.

Abtime uses additional parameters to get around that.

You can still get into situations where testing is difficult or inconventient. However I have not been able to work out an API that is any better or safer, or I'd make a 2.0. Generally what's been provided by this library has been enough.

In my personal experince, the only thing I use the clock advancing for is test stability for code that needs to add time stamps for things. When testing triggers and alarms, it's often perfectly sufficient to just trigger them and not advance the clock at all. It's generally the logic you're testing, and the various paths code can execute through, not actual time handling itself. So in practice I don't generally miss having the nice "just advance the clock" API as much as you would think.

Stability

As I have been using this code for a while now and it has stopped changing, this is now at version 1.0.0.

Commit Signing

Starting with the commit after 3003eee879c, I will be signing this repository with the "jerf" keybase account. If you are viewing this repository through GitHub, you should see the commits as showing as "verified" in the commit view.

(Bear in mind that due to the nature of how git commit signing works, there may be runs of unverified commits; what matters is that the top one is signed.)

Documentation

Overview

Package abtime provides abstracted time functionality that can be swapped between testing and real without changing application code.

In any code that seriously uses time, such as billing or scheduling code, best software engineering practices are that you should not directly access the operating system time.

Other people's discussions: http://blog.plover.com/prog/Moonpig.html#testing-sucks http://stackoverflow.com/questions/5622194/time-dependent-unit-tests/5622222#5622222 http://jim-mcbeath.blogspot.com/2009/02/unit-testing-with-dates-and-times.html

This module wraps the parts of the time module of Go that do access the OS time directly, as it stands at Go 1.2 and 1.3 (which are both the same.) Unfortunately, due to the fact I can not re-export types, you'll still need to import "time" for its types.

This module declares an interface for time functions AbstractTime, provides an implementation that simply backs to the "real" time functions "RealTime", and provides an implementation that allows you to fully control the time "ManualTime", including setting "now", and requiring you to manually trigger all time-based events, such as alerts and alarms.

Since there is no way to distinguish between different calls to the standard time functions, each of the methods in the AbstractTime interface adds an "id". The RealTime implementation simply ignores them. The ManualTime implementations uses these to trigger specific time events. Be sure to see the example for usage of the ManualTime implementation.

Avoid re-using IDs on the Tick functions; it becomes confusing which .Trigger is affecting which Tick.

Be sure to see the Example below.

Quality: At the moment I would call this beta code. Go lint clean, go vet clean, 100% coverage in the tests. You and I both know that doesn't prove this is bug-free, but at least it shows I care. And bear in mind what this really provides is a structure, rather than a whackload of code; should the code prove not quite correct for your project, it will be easy for you to fix it.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AbstractTime

type AbstractTime interface {
	Now() time.Time
	After(time.Duration, int) <-chan time.Time
	Sleep(time.Duration, int)
	Tick(time.Duration, int) <-chan time.Time
	NewTicker(time.Duration, int) Ticker
	AfterFunc(time.Duration, func(), int) Timer
	NewTimer(time.Duration, int) Timer

	WithDeadline(context.Context, time.Time, int) (context.Context, context.CancelFunc)
	WithTimeout(context.Context, time.Duration, int) (context.Context, context.CancelFunc)
}

The AbstractTime interface abstracts the time module into an interface.

Example
package main

import (
	"time"
)

// It's best to allocate IDs like this for your time usages.
const (
	timeoutID = iota
)

func main() {
	// Suppose you have a goroutine feeding you something from a socket,
	// and you want to do something if that times out. You can test this
	// with:
	manualTime := NewManual()
	timedOut := make(chan struct{})

	go ReadSocket(manualTime, timedOut)

	manualTime.Trigger(timeoutID)

	// This will read the struct{}{} from above. Getting here asserts
	// that we did what we wanted when we timed out.
	<-timedOut
}

// In production code, at would be a RealTime, and thus use the "real"
// time.After function, ignoring the ID.
func ReadSocket(at AbstractTime, timedOut chan struct{}) {
	timeout := at.After(time.Second, timeoutID)

	// in this example, this will never be filled
	fromSocket := make(chan []byte)

	select {
	case <-fromSocket:
		// handle socketData
	case <-timeout:
		timedOut <- struct{}{}
	}
}
Output:

type ManualTime

type ManualTime struct {
	sync.Mutex
	// contains filtered or unexported fields
}

The ManualTime object implements a time object you directly control.

This allows you to manipulate "now", and control when events occur.

func NewManual

func NewManual() *ManualTime

NewManual returns a new ManualTime object, with the Now populated from the time.Now().

func NewManualAtTime

func NewManualAtTime(now time.Time) *ManualTime

NewManualAtTime returns a new ManualTime object, with the Now set to the time.Time you pass in.

func (*ManualTime) Advance

func (mt *ManualTime) Advance(d time.Duration)

Advance advances the manual time's idea of "now" by the given duration.

If there is a queue of "Nows" from QueueNows, note this won't affect any of them.

func (*ManualTime) After

func (mt *ManualTime) After(d time.Duration, id int) <-chan time.Time

After wraps time.After, and waits for the target id.

func (*ManualTime) AfterFunc

func (mt *ManualTime) AfterFunc(d time.Duration, f func(), id int) Timer

AfterFunc fires the function in its own goroutine when the id is .Trigger()ed. The resulting Timer object will return nil for its Channel().

func (*ManualTime) NewTicker

func (mt *ManualTime) NewTicker(d time.Duration, id int) Ticker

NewTicker wraps time.NewTicker. It takes a snapshot of "now" at the point of the TickToken call, and will increment the time it returns by the Duration of the tick.

Note that this can cause times to arrive out of order relative to each other if you have many of these going at once, if you manually trigger the ticks in such a way that they will be out of order.

func (*ManualTime) NewTimer

func (mt *ManualTime) NewTimer(d time.Duration, id int) Timer

NewTimer allows you to create a Ticker, which can be triggered via the given id, and also supports the Stop operation *time.Tickers have.

func (*ManualTime) Now

func (mt *ManualTime) Now() time.Time

Now returns the ManualTime's current idea of "Now".

If you have used QueueNow, this will advance to the next queued Now.

func (*ManualTime) QueueNows

func (mt *ManualTime) QueueNows(times ...time.Time)

QueueNows allows you to set a number of times to be retrieved by successive calls to "Now". Once the queue is consumed by calls to Now(), the last time in the queue "sticks" as the new Now.

This is useful if you have code that is timing how long something took by successive calls to .Now, with no other place for the test code to intercede.

If multiple threads are accessing the Manual, it is of course non-deterministic who gets what time. However this could still be useful.

func (*ManualTime) Sleep

func (mt *ManualTime) Sleep(d time.Duration, id int)

Sleep halts execution until you release it via Trigger.

func (*ManualTime) Tick

func (mt *ManualTime) Tick(d time.Duration, id int) <-chan time.Time

Tick allows you to create a ticker. See notes on NewTicker.

func (*ManualTime) Trigger

func (mt *ManualTime) Trigger(ids ...int)

Trigger takes the given ids for time events, and causes them to "occur": triggering messages on channels, ending sleeps, etc.

Note this is the ONLY way to "trigger" such events. While this package allows you to manipulate "Now" in a couple of different ways, advancing "now" past a Trigger's set time will NOT trigger it. First, this keeps it simple to understand when things are triggered, and second, reality isn't so deterministic anyhow....

func (*ManualTime) Unregister added in v1.0.2

func (mt *ManualTime) Unregister(ids ...int)

Unregister will unregister a particular ID from the system. Normally the first one sticks, which means if you've got code that creates multiple timers in a loop or in multiple function calls, only the first one will work.

NOTE: This method indicates a design flaw in abtime. It is not yet clear to me how to fix it in any reasonable way.

func (*ManualTime) UnregisterAll added in v1.0.2

func (mt *ManualTime) UnregisterAll()

UnregisterAll will unregister all current IDs from the manual time, returning you to a fresh view of the created channels and timers and such.

func (*ManualTime) WithDeadline added in v1.0.7

func (mt *ManualTime) WithDeadline(parent context.Context, deadline time.Time, id int) (context.Context, context.CancelFunc)

WithDeadline is a valid Context that is meant to drop in over a regular context.WithDeadline invocation. Instead of being canceled when reaching an actual deadline the context is canceled either by Trigger or by the returned CancelFunc.

func (*ManualTime) WithTimeout added in v1.0.7

func (mt *ManualTime) WithTimeout(parent context.Context, timeout time.Duration, id int) (context.Context, context.CancelFunc)

WithTimeout is equivalent to WithDeadline invoked on a deadline equal to the current time plus the timeout.

type RealTime

type RealTime struct{}

The RealTime object implements the direct calls to the time module.

func NewRealTime

func NewRealTime() RealTime

NewRealTime returns a AbTime-conforming object that backs to the standard time module.

func (RealTime) After

func (rt RealTime) After(d time.Duration, token int) <-chan time.Time

After wraps time.After.

func (RealTime) AfterFunc

func (rt RealTime) AfterFunc(d time.Duration, f func(), token int) Timer

AfterFunc wraps time.AfterFunc. It returns something conforming to the abtime.Timer interface.

func (RealTime) NewTicker

func (rt RealTime) NewTicker(d time.Duration, token int) Ticker

NewTicker wraps time.NewTicker. It returns something conforming to the abtime.Ticker interface.

func (RealTime) NewTimer

func (rt RealTime) NewTimer(d time.Duration, token int) Timer

NewTimer wraps time.NewTimer. It returns something conforming to the abtime.Timer interface.

func (RealTime) Now

func (rt RealTime) Now() time.Time

Now wraps time.Now.

func (RealTime) Sleep

func (rt RealTime) Sleep(d time.Duration, token int)

Sleep wraps time.Sleep.

func (RealTime) Tick

func (rt RealTime) Tick(d time.Duration, token int) <-chan time.Time

Tick wraps time.Tick.

func (RealTime) WithDeadline added in v1.0.7

func (rt RealTime) WithDeadline(parent context.Context, deadline time.Time, _ int) (context.Context, context.CancelFunc)

WithDeadline wraps context's normal WithDeadline invocation.

func (RealTime) WithTimeout added in v1.0.7

func (rt RealTime) WithTimeout(parent context.Context, timeout time.Duration, _ int) (context.Context, context.CancelFunc)

WithTimeout wraps context's normal WithTimeout invocation.

type Ticker

type Ticker interface {
	Channel() <-chan time.Time
	Reset(time.Duration)
	Stop()
}

Ticker defines an interface for the functions that return *time.Ticker in the original Time module.

type Timer

type Timer interface {
	Stop() bool
	Reset(time.Duration) bool
	Channel() <-chan time.Time
}

Timer defines an interface for the functions that return *time.Timer in the original Time module.

type TimerWrap

type TimerWrap struct {
	T *time.Timer
}

TimerWrap wraps a Timer-conforming wrapper around a *time.Timer.

func (TimerWrap) Channel

func (tw TimerWrap) Channel() <-chan time.Time

Channel returns the channel the *time.Timer will signal on.

func (TimerWrap) Reset

func (tw TimerWrap) Reset(d time.Duration) bool

Reset wraps the *time.Timer.Reset().

func (TimerWrap) Stop

func (tw TimerWrap) Stop() bool

Stop wraps the *time.Timer.Stop().

Jump to

Keyboard shortcuts

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