class Dbd::TimeStamp

TimeStamp

Each Fact has a time_stamp with a granularity of 1 ns. The small granularity is essential to allow enough “density” of Facts in a large fact stream. Since all Facts need to have a strictly monotonically increasing time_stamp, this causes a limitation of max 1_000_000_000 Facts per second in a fact stream.

A second reason for a fine grained granularity of the time_stamp is to reduce the chance (but not to zero) for collisions between Facts when 2 (or more) fact streams with overlapping time ranges need to be merged. But, collisions are always possible and need to be handled (since this can be expensive, we need to avoid them).

A practicaly problem with calculating a “randomized” time_stamp is that the system reports a Wall clock with a granularity of 1 us on MRI Ruby and only 1 ms on JRuby (see JSR 310). To solve this problem, some nifty tricks are needed to create more “randomized” time_stamps, while still guaranteeing, the strictly monotonic increase in an upredictable fact stream.

Performance measurements show a typical 30 - 60 us delay between the consecutive created facts (on MRI and JRuby), so a randomization of e.g. 1 - 999 ns should not cause fundamental problems for the density of the facts (even if computers speed up a factor of 30 or an implementation in a faster language). Still this is an ad-hoc optimization at creation time and can be optimized without breaking the specification of the fact stream.

A time_stamp does not need to represent the exact time of the creation of the fact, it only has to increase strictly monotic in a fact stream.

Constants

MAX_DRIFT

Max drift in time_stamp for near?

Attributes

time[R]

Public Class Methods

new(options={}) click to toggle source

Builds a new TimeStamp.

@param [Hash{Symbol => Object}] options @option options [Time, String] :time (Time.now) force the time to this value @option options [TimeStamp] :larger_than (void) time_stamp must be larger than this

# File lib/dbd/time_stamp.rb, line 47
def initialize(options={})
  @time = options[:time] || new_time(options[:larger_than])
  @time = time_from_s(@time) if @time.is_a?(String)
end
valid_regexp() click to toggle source

regexp for the nanosecond granularity and in UTC

Can be used to validate input strings or in tests.

# File lib/dbd/time_stamp.rb, line 56
def self.valid_regexp
  /\A\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{9} UTC\Z/
end

Public Instance Methods

+(seconds) click to toggle source
# File lib/dbd/time_stamp.rb, line 142
def +(seconds)
  self.class.new(time: (@time + seconds))
end
-(other) click to toggle source
# File lib/dbd/time_stamp.rb, line 146
def -(other)
  @time - other.time
end
<(other) click to toggle source
# File lib/dbd/time_stamp.rb, line 130
def <(other)
  @time < other.time
end
<=(other) click to toggle source
# File lib/dbd/time_stamp.rb, line 138
def <=(other)
  @time <= other.time
end
>(other) click to toggle source
# File lib/dbd/time_stamp.rb, line 126
def >(other)
  @time > other.time
end
>=(other) click to toggle source
# File lib/dbd/time_stamp.rb, line 134
def >=(other)
  @time >= other.time
end
near?(other) click to toggle source

determines if 2 time_stamps are “near”.

The time_stamp of an equivalent fact may be slightly different (because shifts of a few nanoseconds will be required to resolve collisions on a merge of fact streams with overlapping time_stamp ranges).

# File lib/dbd/time_stamp.rb, line 122
def near?(other)
  (self - other).abs <= MAX_DRIFT
end
to_s() click to toggle source

with a nanosecond granularity and in UTC

# File lib/dbd/time_stamp.rb, line 108
def to_s
  @time.strftime(time_format)
end

Private Instance Methods

minimum_time_offset() click to toggle source

Minimum offset between consecutive times. For jruby, a 1 ns offset was not enough (some collisions, see github.com/jruby/jruby/issues/843) With 2 ns, the problem disappears (probably a “1 off” rounding error that cannot occur with a minimum distance of 2?).

# File lib/dbd/time_stamp.rb, line 71
def minimum_time_offset
  Rational('2/1_000_000_000')
end
new_time(larger_than) click to toggle source
# File lib/dbd/time_stamp.rb, line 62
def new_time(larger_than)
  [Time.now.utc, (larger_than && larger_than.time)].compact.max + random_offset
end
random_offset() click to toggle source

Random offset, between minimum_time_offset and 1 us (1 micro second)

# File lib/dbd/time_stamp.rb, line 76
def random_offset
  minimum_time_offset + Rational("#{rand(990)}/1_000_000_000")
end
time_format() click to toggle source
# File lib/dbd/time_stamp.rb, line 80
def time_format
  '%F %T.%N %Z'
end
time_from_s(time_string) click to toggle source

with a nanosecond granularity and in UTC

# File lib/dbd/time_stamp.rb, line 85
def time_from_s(time_string)
  # For ns precision in JRuby this extended process is required
  time_hash = DateTime._strptime(time_string, time_format)
  validate_time_zone(time_hash)
  Time.utc(time_hash[:year],
           time_hash[:mon],
           time_hash[:mday],
           time_hash[:hour],
           time_hash[:min],
           time_hash[:sec],
           time_hash[:sec_fraction] * 1_000_000 + Rational('3/10_000_000_000'))
end
validate_time_zone(time_hash) click to toggle source
# File lib/dbd/time_stamp.rb, line 98
def validate_time_zone(time_hash)
  unless time_hash[:zone] == 'UTC'
    raise(ArgumentError, "Time zone was #{time_hash[:zone]}, must be 'UTC'")
  end
end