Class: Waitmate::Store::SolidCache
- Inherits:
-
Object
- Object
- Waitmate::Store::SolidCache
- Defined in:
- lib/waitmate/store/solid_cache.rb
Overview
SQL-backed fallback adapter built on top of SolidCache::Entry.
Solid Cache is a fixed-schema key/value store, so Waitmate queue state is separated into two key prefixes:
waitmate:waiting:{queue}:{identity} — waiting entries
waitmate:active:{queue}:{identity} — admitted entries
This mirrors the Redis adapter's separate sorted sets and lets SQL
LIKE narrow scans to one state before Ruby-side JSON parsing.
Active scans are bounded by max_concurrent; waiting scans are
bounded by queue depth ahead of the target entry.
FIFO ordering relies on an enqueued_at timestamp stored in the JSON
value, avoiding reliance on the binary created_at column which has
cross-Rails-version comparison issues.
Single-key lookups use SolidCache::Entry.read / delete_by_key
which operate on the indexed key_hash integer column, avoiding
binary key column comparison entirely.
Admission is serialized per queue with a mutex row so capacity accounting never follows a read-modify-write path under concurrency.
Constant Summary collapse
- KEY_PREFIX =
"waitmate"
Instance Method Summary collapse
-
#active_count(queue_name) ⇒ Object
active_count(queue_name) -> Integer.
-
#admit(queue_name, max_concurrent, count: max_concurrent) ⇒ Object
admit(queue_name, max_concurrent, count: max_concurrent) -> Array
. -
#enqueue(queue_name, identity, ttl: nil) ⇒ Object
enqueue(queue_name, identity, ttl: nil) -> Integer (1-based position) or 0 (already active).
-
#expire_stale(queue_name) ⇒ Object
expire_stale(queue_name) -> Hash Integer, active: Integer.
-
#heartbeat(queue_name, identity, ttl: nil) ⇒ Object
heartbeat(queue_name, identity, ttl: nil) -> Boolean.
-
#initialize(config: Waitmate.configuration) ⇒ SolidCache
constructor
A new instance of SolidCache.
-
#position(queue_name, identity) ⇒ Object
position(queue_name, identity) -> Integer (1-based), 0 (active), or nil.
-
#release(queue_name, identity) ⇒ Object
release(queue_name, identity) -> Boolean.
Constructor Details
#initialize(config: Waitmate.configuration) ⇒ SolidCache
Returns a new instance of SolidCache.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/waitmate/store/solid_cache.rb', line 38 def initialize(config: Waitmate.configuration) begin require "solid_cache" unless defined?(::SolidCache) rescue LoadError # Fall through to the contract check below. end unless defined?(::SolidCache::Entry) raise ConfigurationError, "Solid Cache gem is not available. Add `gem 'solid_cache'` to your Gemfile " \ "to use the Solid Cache adapter." end @queue_ttl = config.queue_ttl end |
Instance Method Details
#active_count(queue_name) ⇒ Object
active_count(queue_name) -> Integer
120 121 122 123 124 125 126 |
# File 'lib/waitmate/store/solid_cache.rb', line 120 def active_count(queue_name) now = with_ar do active_count_internal(queue_name, now) end end |
#admit(queue_name, max_concurrent, count: max_concurrent) ⇒ Object
admit(queue_name, max_concurrent, count: max_concurrent) -> Array
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/waitmate/store/solid_cache.rb', line 129 def admit(queue_name, max_concurrent, count: max_concurrent) now = active_ttl = @queue_ttl with_ar do admitted = [] ActiveRecord::Base.transaction do ensure_mutex(queue_name) expire_stale_internal(queue_name, now) active = active_count_internal(queue_name, now) available = max_concurrent - active break admitted if available <= 0 admit_count = [available, count].min # Separate waiting prefix guarantees all returned rows are waiting. # No Ruby-side state filter needed — fixes the S2-NEWBUG capacity bug. # Sort by enqueued_at from the JSON value for FIFO ordering. # uncached is required because SolidCache's own write/delete methods do # not dirty the Rails query cache, so a subsequent scan in the same # request (e.g. release → admit) could read stale waiting rows. waiting = ::SolidCache::Entry.uncached do ::SolidCache::Entry .where("key LIKE ?", waiting_key_pattern(queue_name)) .to_a .map { |entry| [entry, parse_value(entry.value, key: entry.key)] } .select { |_, value| value["expires_at"] > now } .sort_by { |_, value| value["enqueued_at"] } .first(admit_count) end waiting.each do |entry, value| identity = identity_from_key(entry.key, queue_name) value["state"] = "active" value["expires_at"] = now + active_ttl ::SolidCache::Entry.delete_by_key(waiting_entry_key(queue_name, identity)) ::SolidCache::Entry.write(active_entry_key(queue_name, identity), value.to_json) admitted << identity end end admitted end end |
#enqueue(queue_name, identity, ttl: nil) ⇒ Object
enqueue(queue_name, identity, ttl: nil) -> Integer (1-based position) or 0 (already active)
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/waitmate/store/solid_cache.rb', line 55 def enqueue(queue_name, identity, ttl: nil) ttl ||= @queue_ttl now = expires_at = now + ttl a_key = active_entry_key(queue_name, identity) w_key = waiting_entry_key(queue_name, identity) with_ar do # Already active and not expired? active_value = read_value(a_key) if active_value return 0 if active_value["expires_at"] > now end # Already waiting and not expired? waiting_value = read_value(w_key) if waiting_value if waiting_value["expires_at"] > now waiting_value["expires_at"] = expires_at ::SolidCache::Entry.write(w_key, waiting_value.to_json) return waiting_position(queue_name, waiting_value["enqueued_at"], now) end # Expired waiting entry — delete so the new write gets a fresh enqueued_at ::SolidCache::Entry.delete_by_key(w_key) end # New entry value = {state: "waiting", expires_at: expires_at, enqueued_at: now} ::SolidCache::Entry.write(w_key, value.to_json) waiting_position(queue_name, now, now) end end |
#expire_stale(queue_name) ⇒ Object
expire_stale(queue_name) -> Hash Integer, active: Integer
217 218 219 220 221 222 223 224 225 226 |
# File 'lib/waitmate/store/solid_cache.rb', line 217 def expire_stale(queue_name) now = with_ar do ActiveRecord::Base.transaction do ensure_mutex(queue_name) expire_stale_internal(queue_name, now) end end end |
#heartbeat(queue_name, identity, ttl: nil) ⇒ Object
heartbeat(queue_name, identity, ttl: nil) -> Boolean
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/waitmate/store/solid_cache.rb', line 187 def heartbeat(queue_name, identity, ttl: nil) ttl ||= @queue_ttl now = w_key = waiting_entry_key(queue_name, identity) a_key = active_entry_key(queue_name, identity) with_ar do # Check waiting first value = read_value(w_key) if value return false if value["expires_at"] <= now value["expires_at"] = now + ttl ::SolidCache::Entry.write(w_key, value.to_json) return true end # Check active value = read_value(a_key) if value return false if value["expires_at"] <= now value["expires_at"] = now + ttl ::SolidCache::Entry.write(a_key, value.to_json) return true end false end end |
#position(queue_name, identity) ⇒ Object
position(queue_name, identity) -> Integer (1-based), 0 (active), or nil
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/waitmate/store/solid_cache.rb', line 89 def position(queue_name, identity) now = a_key = active_entry_key(queue_name, identity) w_key = waiting_entry_key(queue_name, identity) with_ar do # Check active active_value = read_value(a_key) if active_value if active_value["expires_at"] <= now ::SolidCache::Entry.delete_by_key(a_key) return nil end return 0 end # Check waiting waiting_value = read_value(w_key) if waiting_value if waiting_value["expires_at"] <= now ::SolidCache::Entry.delete_by_key(w_key) return nil end return waiting_position(queue_name, waiting_value["enqueued_at"], now) end nil end end |
#release(queue_name, identity) ⇒ Object
release(queue_name, identity) -> Boolean
177 178 179 180 181 182 183 184 |
# File 'lib/waitmate/store/solid_cache.rb', line 177 def release(queue_name, identity) key = active_entry_key(queue_name, identity) with_ar do result = ::SolidCache::Entry.delete_by_key(key) result.is_a?(Integer) ? result > 0 : result end end |