pool/event/event.go

174 lines
6.1 KiB
Go

package event
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"sort"
"time"
"go.etcd.io/bbolt"
)
const (
// TimestampLength is the length in bytes it takes to serialize a
// timestamp with nanosecond precision.
TimestampLength = 8
)
var (
// Big endian is the preferred byte order, due to cursor scans over
// integer keys iterating in order.
byteOrder = binary.BigEndian
)
// Type denotes the type of an event. The numeric representation of the types
// must be unique, that's why we define all of them in this package. But the
// actual implementation is left to the business package.
type Type uint8
const (
// TypeAny denotes no specific type and should only be used for querying
// the event database, not as an actual event type.
TypeAny Type = 0
// TypeOrderCreated is the type of event that is emitted when an order
// is first created.
TypeOrderCreated Type = 1
// TypeOrderStateChange is the type of event that is emitted when an
// order changes its state in the order database due to it being
// modified.
TypeOrderStateChange Type = 2
// TypeOrderMatch is the type of event that is emitted when an order is
// being matched in a batch. An event of this type only denotes a
// participation in a batch attempt and not necessarily final inclusion.
// If a reject happens for any reason, the order might not make it to
// the final batch and not all match states would therefore be present.
TypeOrderMatch Type = 3
)
// Event is the main interface all events have to implement.
type Event interface {
// Type returns the type of the event.
Type() Type
// Timestamp is the time the event happened. This will be made unique
// once it is stored. To avoid collisions, the timestamp is adjusted on
// the nanosecond scale to reach uniqueness.
Timestamp() time.Time
// SetTimestamp updates the timestamp of the event. This is needed to
// adjust timestamps in case they collide to ensure the global
// uniqueness of all event timestamps.
SetTimestamp(time.Time)
// String returns a human readable representation of the event.
String() string
// Serialize writes the event data to a binary storage format. This does
// not serialize the event type as that's handled generically to allow
// for easy filtering.
Serialize(*bytes.Buffer) error
// Deserialize reads the event data from a binary storage format. This
// does not deserialize the event type as that's handled generically to
// allow for easy filtering.
Deserialize(io.Reader) error
}
// Predicate is a function type that can be used to filter events. It gets the
// timestamp and type of an event passed in and can return whether that event
// is relevant or not.
type Predicate func(time.Time, Type) bool
// StoreEvent tries to store an event into the given bucket by trying to avoid
// collisions. If a key for the event timestamp already exists in the database,
// the timestamp is incremented in nanosecond intervals until a "free" slot is
// found. The given scratch space byte slice should ideally point to an array to
// avoid allocating new memory with each iteration. The value found in it after
// the function returns with a nil-error is the actual storage key that was
// used.
//
// NOTE: When storing many events at the same time, the MakeUniqueTimestamps
// function should be called first to ensure the timestamps are already unique
// among themselves. If no "gap" in the timestamps can be found after 1000 tries
// an error is returned.
func StoreEvent(bucket *bbolt.Bucket, event Event) error {
// We use a fixed length array as our buffer so we don't allocate new
// memory for each try.
var tsScratchSpace [TimestampLength]byte
// First, we'll serialize this timestamp into our timestamp buffer.
byteOrder.PutUint64(
tsScratchSpace[:], uint64(event.Timestamp().UnixNano()),
)
// Next we'll loop until we find a "free" slot in the bucket to store
// the event under. This should almost never happen unless we're running
// on a system that has a very bad system clock that doesn't properly
// resolve to nanosecond scale. We try up to 1000 times (which would
// come to a maximum shift of 1 microsecond which is acceptable for most
// use cases). If we don't find a free slot, we don't want to lose any
// data by just overwriting an existing event and therefore throw an
// error.
const maxTries = 1000
tries := 0
for tries < maxTries {
val := bucket.Get(tsScratchSpace[:])
if val == nil {
break
}
// Collision, try the next nanosecond timestamp.
nextNano := event.Timestamp().UnixNano() + 1
event.SetTimestamp(time.Unix(0, nextNano))
byteOrder.PutUint64(tsScratchSpace[:], uint64(nextNano))
tries++
}
if tries == maxTries {
return fmt.Errorf("error finding unique timestamp slot for "+
"event ts=%v, type=%v", event.Timestamp(), event.Type())
}
// With the key encoded, we'll then encode the event
// into our buffer, then write it out to disk.
var eventBuf bytes.Buffer
if err := eventBuf.WriteByte(byte(event.Type())); err != nil {
return err
}
err := event.Serialize(&eventBuf)
if err != nil {
return err
}
return bucket.Put(tsScratchSpace[:], eventBuf.Bytes())
}
// MakeUniqueTimestamps takes a slice of event records, sorts it by the event
// timestamps and then makes sure there are no duplicates in the timestamps. If
// duplicates are found, some of the timestamps are increased on the nanosecond
// scale until only unique values remain.
func MakeUniqueTimestamps(events []Event) {
sort.Slice(events, func(i, j int) bool {
return events[i].Timestamp().Before(events[j].Timestamp())
})
// Now that we know the events are sorted by timestamp, we can go
// through the list and fix all duplicates until only unique values
// remain.
for i := 0; i < len(events)-1; i++ {
current := events[i].Timestamp().UnixNano()
next := events[i+1].Timestamp().UnixNano()
// We initially sorted the slice. So if the current is now
// greater or equal to the next one, it's either because it's a
// duplicate or because we increased the current in the last
// iteration.
if current >= next {
next = current + 1
events[i+1].SetTimestamp(time.Unix(0, next))
}
}
}