zermelo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +512 -0
- data/Rakefile +1 -0
- data/lib/zermelo/associations/association_data.rb +24 -0
- data/lib/zermelo/associations/belongs_to.rb +115 -0
- data/lib/zermelo/associations/class_methods.rb +244 -0
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
- data/lib/zermelo/associations/has_many.rb +120 -0
- data/lib/zermelo/associations/has_one.rb +109 -0
- data/lib/zermelo/associations/has_sorted_set.rb +124 -0
- data/lib/zermelo/associations/index.rb +50 -0
- data/lib/zermelo/associations/index_data.rb +18 -0
- data/lib/zermelo/associations/unique_index.rb +44 -0
- data/lib/zermelo/backends/base.rb +115 -0
- data/lib/zermelo/backends/influxdb_backend.rb +178 -0
- data/lib/zermelo/backends/redis_backend.rb +281 -0
- data/lib/zermelo/filters/base.rb +235 -0
- data/lib/zermelo/filters/influxdb_filter.rb +162 -0
- data/lib/zermelo/filters/redis_filter.rb +558 -0
- data/lib/zermelo/filters/steps/base_step.rb +22 -0
- data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/diff_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
- data/lib/zermelo/filters/steps/limit_step.rb +17 -0
- data/lib/zermelo/filters/steps/offset_step.rb +17 -0
- data/lib/zermelo/filters/steps/sort_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_step.rb +17 -0
- data/lib/zermelo/locks/no_lock.rb +16 -0
- data/lib/zermelo/locks/redis_lock.rb +221 -0
- data/lib/zermelo/records/base.rb +62 -0
- data/lib/zermelo/records/class_methods.rb +127 -0
- data/lib/zermelo/records/collection.rb +14 -0
- data/lib/zermelo/records/errors.rb +24 -0
- data/lib/zermelo/records/influxdb_record.rb +35 -0
- data/lib/zermelo/records/instance_methods.rb +224 -0
- data/lib/zermelo/records/key.rb +19 -0
- data/lib/zermelo/records/redis_record.rb +27 -0
- data/lib/zermelo/records/type_validator.rb +20 -0
- data/lib/zermelo/version.rb +3 -0
- data/lib/zermelo.rb +102 -0
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
- data/spec/lib/zermelo/associations/index_spec.rb +6 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
- data/spec/lib/zermelo/records/key_spec.rb +6 -0
- data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
- data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
- data/spec/lib/zermelo/version_spec.rb +6 -0
- data/spec/lib/zermelo_spec.rb +6 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/profile_all_formatter.rb +44 -0
- data/spec/support/uncolored_doc_formatter.rb +74 -0
- data/zermelo.gemspec +30 -0
- metadata +174 -0
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require 'zermelo/filters/steps/base_step'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zermelo
         | 
| 4 | 
            +
              module Filters
         | 
| 5 | 
            +
                class Steps
         | 
| 6 | 
            +
                  class SortStep < Zermelo::Filters::Steps::BaseStep
         | 
| 7 | 
            +
                    def self.accepted_types
         | 
| 8 | 
            +
                      [:set, :sorted_set]
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def self.returns_type
         | 
| 12 | 
            +
                      :list
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require 'zermelo/filters/steps/base_step'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zermelo
         | 
| 4 | 
            +
              module Filters
         | 
| 5 | 
            +
                class Steps
         | 
| 6 | 
            +
                  class UnionRangeStep < Zermelo::Filters::Steps::BaseStep
         | 
| 7 | 
            +
                    def self.accepted_types
         | 
| 8 | 
            +
                      [:sorted_set]
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def self.returns_type
         | 
| 12 | 
            +
                      :sorted_set
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require 'zermelo/filters/steps/base_step'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zermelo
         | 
| 4 | 
            +
              module Filters
         | 
| 5 | 
            +
                class Steps
         | 
| 6 | 
            +
                  class UnionStep < Zermelo::Filters::Steps::BaseStep
         | 
| 7 | 
            +
                    def self.accepted_types
         | 
| 8 | 
            +
                      [:set, :sorted_set]
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def self.returns_type
         | 
| 12 | 
            +
                      :set
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,221 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Zermelo
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              class LockNotAcquired < StandardError; end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              module Locks
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                class RedisLock
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # Adapted from https://github.com/mlanett/redis-lock,
         | 
| 11 | 
            +
                  # now covers locking multiple keys at once
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  attr_accessor :expires_at, :life, :sleep_in_ms
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize
         | 
| 16 | 
            +
                    @owner_value = Thread.current.object_id
         | 
| 17 | 
            +
                    @life        = 60
         | 
| 18 | 
            +
                    @timeout     = 10
         | 
| 19 | 
            +
                    @sleep_in_ms = 125
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def lock(*record_klasses, &block)
         | 
| 23 | 
            +
                    @keys = record_klasses.map{|k| k.send(:class_key) }.sort.map{|k| "#{k}::lock" }
         | 
| 24 | 
            +
                    do_lock_with_timeout(@timeout) or raise Zermelo::LockNotAcquired.new(@keys.join(", "))
         | 
| 25 | 
            +
                    result = true
         | 
| 26 | 
            +
                    if block
         | 
| 27 | 
            +
                      begin
         | 
| 28 | 
            +
                        result = (block.arity == 1) ? block.call(self) : block.call
         | 
| 29 | 
            +
                      # rescue Exception => e
         | 
| 30 | 
            +
                      #   puts e.message
         | 
| 31 | 
            +
                      #   puts e.backtrace.join("\n")
         | 
| 32 | 
            +
                      #   raise e
         | 
| 33 | 
            +
                      ensure
         | 
| 34 | 
            +
                        release_lock
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                    result
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def extend_life( new_life )
         | 
| 41 | 
            +
                    do_extend( new_life ) or raise Zermelo::LockNotAcquired.new(@keys.join(", "))
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def unlock
         | 
| 45 | 
            +
                    release_lock
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  private
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def full_keys
         | 
| 51 | 
            +
                    @full_keys ||= @keys.map {|k| ["#{k}:owner", "#{k}:expiry"] }.flatten
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def owner_keys
         | 
| 55 | 
            +
                    @owner_keys ||= @keys.map {|k| "#{k}:owner" }
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def expiry_keys
         | 
| 59 | 
            +
                    @expiry_keys ||= @keys.map {|k| "#{k}:expiry" }
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def do_lock_with_timeout( timeout )
         | 
| 63 | 
            +
                    locked = false
         | 
| 64 | 
            +
                    with_timeout(timeout) { locked = do_lock }
         | 
| 65 | 
            +
                    locked
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  # @returns true if locked, false otherwise
         | 
| 69 | 
            +
                  def do_lock( tries = 3 )
         | 
| 70 | 
            +
                    # We need to set both owner and expire at the same time
         | 
| 71 | 
            +
                    # If the existing lock is stale, we delete it and try again once
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    locked = nil
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    loop do
         | 
| 76 | 
            +
                      new_xval = Time.now.to_i + @life
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      lock_keyvals = @keys.map {|k| ["#{k}:owner",  @owner_value,
         | 
| 79 | 
            +
                                                     "#{k}:expiry", new_xval] }.flatten
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      result = Zermelo.redis.msetnx(*lock_keyvals)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                      if [1, true].include?(result)
         | 
| 84 | 
            +
                        # log :debug, "do_lock() success"
         | 
| 85 | 
            +
                        @expires_at = new_xval
         | 
| 86 | 
            +
                        locked = true
         | 
| 87 | 
            +
                        break
         | 
| 88 | 
            +
                      else
         | 
| 89 | 
            +
                        # log :debug, "do_lock() failed"
         | 
| 90 | 
            +
                        # consider the possibility that this lock is stale
         | 
| 91 | 
            +
                        tries -= 1
         | 
| 92 | 
            +
                        next if tries > 0 && stale_key?
         | 
| 93 | 
            +
                        locked = false
         | 
| 94 | 
            +
                        break
         | 
| 95 | 
            +
                      end
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                    locked
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def do_extend( new_life )
         | 
| 101 | 
            +
                    # We use watch and a transaction to ensure we only change a lock we own
         | 
| 102 | 
            +
                    # The transaction fails if the watched variable changed
         | 
| 103 | 
            +
                    # Use my_owner = oval to make testing easier.
         | 
| 104 | 
            +
                    new_xval = Time.now.to_i + new_life
         | 
| 105 | 
            +
                    extended = false
         | 
| 106 | 
            +
                    with_watch( *owner_keys  ) do
         | 
| 107 | 
            +
                      owners = Zermelo.redis.mget( *owner_keys )
         | 
| 108 | 
            +
                      if owners == ([@owner_value.to_s] * owner_keys.size)
         | 
| 109 | 
            +
                        result = Zermelo.redis.multi do |multi|
         | 
| 110 | 
            +
                          multi.mset( *(expiry_keys.zip( [new_xval] * expiry_keys.size)) )
         | 
| 111 | 
            +
                        end
         | 
| 112 | 
            +
                        if result == ['OK']
         | 
| 113 | 
            +
                          # log :debug, "do_extend() success"
         | 
| 114 | 
            +
                          @expires_at = new_xval
         | 
| 115 | 
            +
                          extended = true
         | 
| 116 | 
            +
                        end
         | 
| 117 | 
            +
                      end
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                    extended
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  # Only actually deletes it if we own it.
         | 
| 123 | 
            +
                  # There may be strange cases where we fail to delete it, in which case expiration will solve the problem.
         | 
| 124 | 
            +
                  def release_lock
         | 
| 125 | 
            +
                    released = false
         | 
| 126 | 
            +
                    with_watch( *full_keys ) do
         | 
| 127 | 
            +
                      owners = Zermelo.redis.mget( *owner_keys )
         | 
| 128 | 
            +
                      if owners == ([@owner_value.to_s] * owner_keys.size)
         | 
| 129 | 
            +
                        result = Zermelo.redis.multi do |multi|
         | 
| 130 | 
            +
                          multi.del(*full_keys)
         | 
| 131 | 
            +
                        end
         | 
| 132 | 
            +
                        if result && (result.size == 1) && (result.first == full_keys.size)
         | 
| 133 | 
            +
                          released = true
         | 
| 134 | 
            +
                        end
         | 
| 135 | 
            +
                      end
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                    released
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  def stale_key?
         | 
| 141 | 
            +
                    # Check if expiration exists and is it stale?
         | 
| 142 | 
            +
                    # If so, delete it.
         | 
| 143 | 
            +
                    # watch() all keys so we can detect if they change while we do this
         | 
| 144 | 
            +
                    # multi() will fail if keys have changed after watch()
         | 
| 145 | 
            +
                    # Thus, we snapshot consistency at the time of watch()
         | 
| 146 | 
            +
                    # Note: inside a watch() we get one and only one multi()
         | 
| 147 | 
            +
                    now = Time.now.to_i
         | 
| 148 | 
            +
                    stale = false
         | 
| 149 | 
            +
                    with_watch( *full_keys ) do
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                      owners_expires = Zermelo.redis.mget(full_keys)
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                      if owners_expires.each_slice(2).all? {|owner, expire| is_deletable?( owner, expire, now)}
         | 
| 154 | 
            +
                        result = Zermelo.redis.multi do |multi|
         | 
| 155 | 
            +
                          multi.del(*full_keys)
         | 
| 156 | 
            +
                        end
         | 
| 157 | 
            +
                        # If anything changed then multi() fails and returns nil
         | 
| 158 | 
            +
                        if result && (result.size == 1) && (result.first == owner_keys.size)
         | 
| 159 | 
            +
                          # log :info, "Deleted stale key from #{owner}"
         | 
| 160 | 
            +
                          stale = true
         | 
| 161 | 
            +
                        end
         | 
| 162 | 
            +
                      end
         | 
| 163 | 
            +
                    end # watch
         | 
| 164 | 
            +
                    stale
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  def locked?
         | 
| 168 | 
            +
                    now = Time.now.to_i
         | 
| 169 | 
            +
                    owners_expires = Zermelo.redis.mget(full_keys)
         | 
| 170 | 
            +
                    owners_expires && (owners_expires.size == (@keys.size * 2)) &&
         | 
| 171 | 
            +
                      owners_expires.each_slice(2).all? {|owner, expiration| is_locked?(owner, expiration, now)}
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  # returns true if the lock exists and is owned by the given owner
         | 
| 175 | 
            +
                  def is_locked?(owner, expiration, now)
         | 
| 176 | 
            +
                    (owner == @owner_value) && ! is_deletable?(owner, expiration, now)
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  # returns true if this is a broken or expired lock
         | 
| 180 | 
            +
                  def is_deletable?( owner, expiration, now)
         | 
| 181 | 
            +
                    expiration = expiration.to_i
         | 
| 182 | 
            +
                    (owner || (expiration > 0)) && (!owner || (expiration < now))
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  def with_watch( *args, &block )
         | 
| 186 | 
            +
                    Zermelo.redis.watch( *args )
         | 
| 187 | 
            +
                    begin
         | 
| 188 | 
            +
                      block.call
         | 
| 189 | 
            +
                    ensure
         | 
| 190 | 
            +
                      Zermelo.redis.unwatch
         | 
| 191 | 
            +
                    end
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  # Calls block until it returns true or times out.
         | 
| 195 | 
            +
                  # @param block should return true if successful, false otherwise
         | 
| 196 | 
            +
                  # @returns true if successful, false otherwise
         | 
| 197 | 
            +
                  def with_timeout( timeout, &block )
         | 
| 198 | 
            +
                    expire = Time.now + timeout.to_f
         | 
| 199 | 
            +
                    sleepy = @sleep_in_ms / 1000.to_f()
         | 
| 200 | 
            +
                    # this looks inelegant compared to while Time.now < expire, but does not oversleep
         | 
| 201 | 
            +
                    ret = nil
         | 
| 202 | 
            +
                    loop do
         | 
| 203 | 
            +
                      if block.call
         | 
| 204 | 
            +
                        ret = true
         | 
| 205 | 
            +
                        break
         | 
| 206 | 
            +
                      end
         | 
| 207 | 
            +
                      if (Time.now + sleepy) > expire
         | 
| 208 | 
            +
                        ret = false
         | 
| 209 | 
            +
                        break
         | 
| 210 | 
            +
                      end
         | 
| 211 | 
            +
                      sleep(sleepy)
         | 
| 212 | 
            +
                      # might like a different strategy, but general goal is not use 100% cpu while contending for a lock.
         | 
| 213 | 
            +
                    end
         | 
| 214 | 
            +
                    ret
         | 
| 215 | 
            +
                  end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
              end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
            end
         | 
| @@ -0,0 +1,62 @@ | |
| 1 | 
            +
            require 'monitor'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'active_support/concern'
         | 
| 4 | 
            +
            require 'active_support/core_ext/object/blank'
         | 
| 5 | 
            +
            require 'active_support/inflector'
         | 
| 6 | 
            +
            require 'active_model'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require 'zermelo/associations/class_methods'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            require 'zermelo/records/instance_methods'
         | 
| 11 | 
            +
            require 'zermelo/records/class_methods'
         | 
| 12 | 
            +
            require 'zermelo/records/type_validator'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            # TODO escape ids and index_keys -- shouldn't allow bare :
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            # TODO callbacks on before/after add/delete on association?
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            # TODO optional sort via Redis SORT, first/last for has_many via those
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            # TODO get DIFF working for exclusion case against ZSETs
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            module Zermelo
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              module Records
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                module Base
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  extend ActiveSupport::Concern
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  include Zermelo::Records::InstMethods
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  included do
         | 
| 33 | 
            +
                    include ActiveModel::AttributeMethods
         | 
| 34 | 
            +
                    extend ActiveModel::Callbacks
         | 
| 35 | 
            +
                    include ActiveModel::Dirty
         | 
| 36 | 
            +
                    include ActiveModel::Validations
         | 
| 37 | 
            +
                    include ActiveModel::Validations::Callbacks
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    # include ActiveModel::MassAssignmentSecurity
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    @lock = Monitor.new
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    extend Zermelo::Records::ClassMethods
         | 
| 44 | 
            +
                    extend Zermelo::Associations::ClassMethods
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    attr_accessor :attributes
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    define_model_callbacks :create, :update, :destroy
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    attribute_method_suffix  "="  # attr_writers
         | 
| 51 | 
            +
                    # attribute_method_suffix  ""   # attr_readers # DEPRECATED
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    validates_with Zermelo::Records::TypeValidator
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    define_attributes :id => :string
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            end
         | 
| @@ -0,0 +1,127 @@ | |
| 1 | 
            +
            require 'forwardable'
         | 
| 2 | 
            +
            require 'securerandom'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'zermelo'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'zermelo/backends/influxdb_backend'
         | 
| 7 | 
            +
            require 'zermelo/backends/redis_backend'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require 'zermelo/records/key'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            module Zermelo
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              module Records
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                module ClassMethods
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  extend Forwardable
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def_delegators :filter, :intersect, :union, :diff, :sort,
         | 
| 20 | 
            +
                                   :find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
         | 
| 21 | 
            +
                                   :page, :all, :each, :collect, :map,
         | 
| 22 | 
            +
                                   :select, :find_all, :reject, :destroy_all,
         | 
| 23 | 
            +
                                   :ids, :count, :empty?, :exists?,
         | 
| 24 | 
            +
                                   :associated_ids_for
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def generate_id
         | 
| 27 | 
            +
                    return SecureRandom.uuid if SecureRandom.respond_to?(:uuid)
         | 
| 28 | 
            +
                    # from 1.9 stdlib
         | 
| 29 | 
            +
                    ary = SecureRandom.random_bytes(16).unpack("NnnnnN")
         | 
| 30 | 
            +
                    ary[2] = (ary[2] & 0x0fff) | 0x4000
         | 
| 31 | 
            +
                    ary[3] = (ary[3] & 0x3fff) | 0x8000
         | 
| 32 | 
            +
                    "%08x-%04x-%04x-%04x-%04x%08x" % ary
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def add_id(id)
         | 
| 36 | 
            +
                    backend.add(ids_key, id.to_s)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def delete_id(id)
         | 
| 40 | 
            +
                    backend.delete(ids_key, id.to_s)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def attribute_types
         | 
| 44 | 
            +
                    ret = nil
         | 
| 45 | 
            +
                    @lock.synchronize do
         | 
| 46 | 
            +
                      ret = (@attribute_types ||= {}).dup
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                    ret
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def lock(*klasses, &block)
         | 
| 52 | 
            +
                    klasses += [self] unless klasses.include?(self)
         | 
| 53 | 
            +
                    backend.lock(*klasses, &block)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def transaction(&block)
         | 
| 57 | 
            +
                    failed = false
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    backend.begin_transaction
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    begin
         | 
| 62 | 
            +
                      yield
         | 
| 63 | 
            +
                    rescue Exception => e
         | 
| 64 | 
            +
                      backend.abort_transaction
         | 
| 65 | 
            +
                      p e.message
         | 
| 66 | 
            +
                      puts e.backtrace.join("\n")
         | 
| 67 | 
            +
                      failed = true
         | 
| 68 | 
            +
                    ensure
         | 
| 69 | 
            +
                      backend.commit_transaction unless failed
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    # TODO include exception info
         | 
| 73 | 
            +
                    raise "Transaction failed" if failed
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def backend
         | 
| 77 | 
            +
                    raise "No data storage backend set for #{self.name}" if @backend.nil?
         | 
| 78 | 
            +
                    @backend
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  protected
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def define_attributes(options = {})
         | 
| 84 | 
            +
                    options.each_pair do |key, value|
         | 
| 85 | 
            +
                      raise "Unknown attribute type ':#{value}' for ':#{key}'" unless
         | 
| 86 | 
            +
                        Zermelo.valid_type?(value)
         | 
| 87 | 
            +
                      self.define_attribute_methods([key])
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                    @lock.synchronize do
         | 
| 90 | 
            +
                      (@attribute_types ||= {}).update(options)
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def set_backend(backend_type)
         | 
| 95 | 
            +
                    @backend ||= case backend_type.to_sym
         | 
| 96 | 
            +
                    when :influxdb
         | 
| 97 | 
            +
                      Zermelo::Backends::InfluxDBBackend.new
         | 
| 98 | 
            +
                    when :redis
         | 
| 99 | 
            +
                      Zermelo::Backends::RedisBackend.new
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  private
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  def ids_key
         | 
| 106 | 
            +
                    @ids_key ||= Zermelo::Records::Key.new(:klass => class_key, :name => 'ids',
         | 
| 107 | 
            +
                      :type => :set, :object => :attribute)
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  def class_key
         | 
| 111 | 
            +
                    self.name.demodulize.underscore
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  def load(id)
         | 
| 115 | 
            +
                    object = self.new
         | 
| 116 | 
            +
                    object.load(id) ? object : nil
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def filter
         | 
| 120 | 
            +
                    backend.filter(ids_key, self)
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
            end
         | 
| @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            module Zermelo
         | 
| 2 | 
            +
              module Records
         | 
| 3 | 
            +
                # high-level abstraction for a set or list of record ids
         | 
| 4 | 
            +
                class Collection
         | 
| 5 | 
            +
                  attr_reader :klass, :name, :type
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def initialize(opts = {})
         | 
| 8 | 
            +
                    @klass  = opts[:class]
         | 
| 9 | 
            +
                    @name   = opts[:name]
         | 
| 10 | 
            +
                    @type   = opts[:type]
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module Zermelo
         | 
| 2 | 
            +
              module Records
         | 
| 3 | 
            +
                module Errors
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  class RecordNotFound < RuntimeError
         | 
| 6 | 
            +
                    attr_reader :klass, :id
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    def initialize(k, i)
         | 
| 9 | 
            +
                      @klass = k
         | 
| 10 | 
            +
                      @id = i
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  class RecordsNotFound < RuntimeError
         | 
| 15 | 
            +
                    attr_reader :klass, :ids
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def initialize(k, i)
         | 
| 18 | 
            +
                      @klass = k
         | 
| 19 | 
            +
                      @ids = i
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
               end
         | 
| 23 | 
            +
             end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require 'active_support/concern'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'zermelo/records/base'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            # a record is a row in a time series (named for the record class)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            # all attributes are stored as fields in that row
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # a save will delete (if required) and create the row
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            # if time field does not exist, this will be created automatically by influxdb
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            # indexing -- not really relevant until query building has been worked on, but
         | 
| 14 | 
            +
            # everything in the influxdb query language should be supportable, maybe those
         | 
| 15 | 
            +
            # just indicate what should be queryable?
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            # TODO: ensure time_precision is set for the incoming data
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # class level values are in other time series (with similar names to the
         | 
| 20 | 
            +
            # related redis sets)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            module Zermelo
         | 
| 23 | 
            +
              module Records
         | 
| 24 | 
            +
                module InfluxDBRecord
         | 
| 25 | 
            +
                  extend ActiveSupport::Concern
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  include Zermelo::Records::Base
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  included do
         | 
| 30 | 
            +
                    set_backend :influxdb
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         |