ask-state-providers 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/ask/state/memory.rb +201 -0
- data/lib/ask/state/providers/mysql.rb +30 -0
- data/lib/ask/state/providers/postgres.rb +34 -1
- data/lib/ask/state/providers/redis.rb +11 -1
- data/lib/ask/state/providers/sqlite.rb +155 -98
- data/lib/ask/state/providers/version.rb +1 -1
- data/lib/ask-state-providers.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 550bf5d05277f2107b94f57c09ec2189184af18ceb820a1538b07a1ed5b3981f
|
|
4
|
+
data.tar.gz: 3bb373e4312de695a6e71d76b3557e5933cc97edeae170cf3ff38c1247010c47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9641c185a5128f8129e28094b8b7c605a5abc458f32685abdb7f121b18991c2eed62e61e37ce29d254e03310c0628f6bb3e98575877887e55e0cd8f0ea55843a
|
|
7
|
+
data.tar.gz: 823cd11970169cf011575f498f1c182a6ed1c76053ccf6cc00f28e840f68ab2dd80b84af9ae19dba474c6a250c284ea97ada092ed2d816979ad242c1457499f3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] — 2026-07-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`Ask::State::Memory`** — in-memory state backend, moved from `ask-core`. Same class name, same API. Requires `require "ask-state-providers"` instead of `require "ask"`.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "ask-state-providers"
|
|
13
|
+
store = Ask::State::Memory.new
|
|
14
|
+
```
|
|
15
|
+
|
|
5
16
|
## [0.2.0] — 2026-07-23
|
|
6
17
|
|
|
7
18
|
### Changed
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module State
|
|
7
|
+
# In-process state backend backed by a Hash.
|
|
8
|
+
# All operations are thread-safe via a Mutex.
|
|
9
|
+
# Data is not persisted — lost on process exit.
|
|
10
|
+
#
|
|
11
|
+
# Available automatically when using +ask-state-providers+.
|
|
12
|
+
# For lightweight use without the full providers gem, see
|
|
13
|
+
# {Ask::State::Adapter} for the abstract contract.
|
|
14
|
+
class Memory < Adapter
|
|
15
|
+
def initialize
|
|
16
|
+
@data = {}
|
|
17
|
+
@locks = {}
|
|
18
|
+
@queues = {}
|
|
19
|
+
@lists = {}
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# -- key-value --
|
|
24
|
+
|
|
25
|
+
def get(key)
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
expiry = @data[key]&.dig(:expires_at)
|
|
28
|
+
return nil if expiry && Time.now >= expiry
|
|
29
|
+
|
|
30
|
+
@data[key]&.dig(:value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set(key, value, ttl: nil)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@data[key] = {
|
|
37
|
+
value: value,
|
|
38
|
+
expires_at: ttl ? Time.now + ttl : nil
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(key)
|
|
44
|
+
@mutex.synchronize { @data.delete(key) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def set_if_not_exists(key, value, ttl: nil)
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
if @data.key?(key)
|
|
50
|
+
expiry = @data[key][:expires_at]
|
|
51
|
+
return false if expiry.nil? || Time.now < expiry
|
|
52
|
+
@data.delete(key)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@data[key] = {
|
|
56
|
+
value: value,
|
|
57
|
+
expires_at: ttl ? Time.now + ttl : nil
|
|
58
|
+
}
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def clear
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
@data.clear
|
|
66
|
+
@locks.clear
|
|
67
|
+
@queues.clear
|
|
68
|
+
@lists.clear
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def exists?(key)
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
expiry = @data[key]&.dig(:expires_at)
|
|
75
|
+
return false if expiry && Time.now >= expiry
|
|
76
|
+
@data.key?(key)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def keys(pattern: nil)
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
active = @data.select do |_k, v|
|
|
83
|
+
expiry = v[:expires_at]
|
|
84
|
+
expiry.nil? || Time.now < expiry
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
keys = active.keys.map(&:to_s)
|
|
88
|
+
return keys unless pattern
|
|
89
|
+
|
|
90
|
+
regex = self.class.glob_to_regex(pattern)
|
|
91
|
+
keys.select { |k| k.match?(regex) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# -- locking --
|
|
96
|
+
|
|
97
|
+
def acquire_lock(key, ttl: 10)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
existing = @locks[key]
|
|
100
|
+
if existing.nil? || existing.expired?
|
|
101
|
+
lock = Lock.new(
|
|
102
|
+
id: key,
|
|
103
|
+
token: SecureRandom.hex(16),
|
|
104
|
+
expires_at: Time.now + ttl
|
|
105
|
+
)
|
|
106
|
+
@locks[key] = lock
|
|
107
|
+
lock
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def release_lock(key, lock)
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
current = @locks[key]
|
|
115
|
+
if current && current.token == lock.token && !current.expired?
|
|
116
|
+
@locks.delete(key)
|
|
117
|
+
true
|
|
118
|
+
else
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# -- queues --
|
|
125
|
+
|
|
126
|
+
def enqueue(queue, value)
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
@queues[queue] ||= []
|
|
129
|
+
entry = QueueEntry.new(
|
|
130
|
+
id: SecureRandom.uuid,
|
|
131
|
+
value: value,
|
|
132
|
+
enqueued_at: Time.now
|
|
133
|
+
)
|
|
134
|
+
@queues[queue] << entry
|
|
135
|
+
entry
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def dequeue(queue)
|
|
140
|
+
@mutex.synchronize do
|
|
141
|
+
q = @queues[queue]
|
|
142
|
+
return nil unless q&.any?
|
|
143
|
+
q.shift
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def queue_depth(queue)
|
|
148
|
+
@mutex.synchronize do
|
|
149
|
+
(@queues[queue] || []).length
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# -- lists --
|
|
154
|
+
|
|
155
|
+
def list_append(key, value, max_length: nil)
|
|
156
|
+
@mutex.synchronize do
|
|
157
|
+
@lists[key] ||= []
|
|
158
|
+
@lists[key] << value
|
|
159
|
+
@lists[key].shift if max_length && @lists[key].length > max_length
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def list_range(key, start = 0, stop = -1)
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
list = @lists[key] || []
|
|
166
|
+
return list if start == 0 && stop == -1
|
|
167
|
+
stop = list.length - 1 if stop == -1 || stop >= list.length
|
|
168
|
+
return [] if start > stop
|
|
169
|
+
list[start..stop] || []
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def list_remove(key, value)
|
|
174
|
+
@mutex.synchronize do
|
|
175
|
+
list = @lists[key]
|
|
176
|
+
return 0 unless list
|
|
177
|
+
before = list.length
|
|
178
|
+
list.delete(value)
|
|
179
|
+
before - list.length
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# -- lifecycle --
|
|
184
|
+
|
|
185
|
+
def close
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
@data.clear
|
|
188
|
+
@locks.clear
|
|
189
|
+
@queues.clear
|
|
190
|
+
@lists.clear
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def glob_to_regex(pattern)
|
|
197
|
+
self.class.glob_to_regex(pattern)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -114,6 +114,36 @@ module Ask
|
|
|
114
114
|
|
|
115
115
|
def clear
|
|
116
116
|
@client.query("DELETE FROM state_store")
|
|
117
|
+
@client.query("DELETE FROM locks")
|
|
118
|
+
@client.query("DELETE FROM queues")
|
|
119
|
+
@client.query("DELETE FROM lists")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def exists?(key)
|
|
123
|
+
result = @client.prepare(<<~SQL).execute(key, Time.now.utc.strftime("%Y-%m-%d %H:%M:%S.%3N"))
|
|
124
|
+
SELECT 1 FROM state_store
|
|
125
|
+
WHERE `key` = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
126
|
+
SQL
|
|
127
|
+
result.count > 0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def keys(pattern: nil)
|
|
131
|
+
sql, params = if pattern
|
|
132
|
+
like = self.class.glob_to_like(pattern)
|
|
133
|
+
[<<~SQL, [like]]
|
|
134
|
+
SELECT `key` FROM state_store
|
|
135
|
+
WHERE `key` LIKE ? AND (expires_at IS NULL OR expires_at > ?)
|
|
136
|
+
SQL
|
|
137
|
+
else
|
|
138
|
+
[<<~SQL, []]
|
|
139
|
+
SELECT `key` FROM state_store
|
|
140
|
+
WHERE (expires_at IS NULL OR expires_at > ?)
|
|
141
|
+
SQL
|
|
142
|
+
end
|
|
143
|
+
now = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
144
|
+
stmt = @client.prepare(sql)
|
|
145
|
+
rows = stmt.execute(*params, now)
|
|
146
|
+
rows.map { |r| r["key"] }
|
|
117
147
|
end
|
|
118
148
|
|
|
119
149
|
# -- distributed locking --
|
|
@@ -116,7 +116,40 @@ module Ask
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def clear
|
|
119
|
-
@pool.with
|
|
119
|
+
@pool.with do |conn|
|
|
120
|
+
conn.exec("DELETE FROM state_store")
|
|
121
|
+
conn.exec("DELETE FROM locks")
|
|
122
|
+
conn.exec("DELETE FROM queues")
|
|
123
|
+
conn.exec("DELETE FROM lists")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def exists?(key)
|
|
128
|
+
@pool.with do |conn|
|
|
129
|
+
result = conn.exec_params(<<~SQL, [key, Time.now.utc])
|
|
130
|
+
SELECT 1 FROM state_store
|
|
131
|
+
WHERE key = $1 AND (expires_at IS NULL OR expires_at > $2)
|
|
132
|
+
SQL
|
|
133
|
+
result.ntuples > 0
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def keys(pattern: nil)
|
|
138
|
+
@pool.with do |conn|
|
|
139
|
+
sql, params = if pattern
|
|
140
|
+
like = self.class.glob_to_like(pattern)
|
|
141
|
+
[<<~SQL, [like, Time.now.utc]]
|
|
142
|
+
SELECT key FROM state_store
|
|
143
|
+
WHERE key LIKE $1 AND (expires_at IS NULL OR expires_at > $2)
|
|
144
|
+
SQL
|
|
145
|
+
else
|
|
146
|
+
[<<~SQL, [Time.now.utc]]
|
|
147
|
+
SELECT key FROM state_store
|
|
148
|
+
WHERE (expires_at IS NULL OR expires_at > $1)
|
|
149
|
+
SQL
|
|
150
|
+
end
|
|
151
|
+
conn.exec_params(sql, params).map { |r| r["key"] }
|
|
152
|
+
end
|
|
120
153
|
end
|
|
121
154
|
|
|
122
155
|
# -- distributed locking --
|
|
@@ -58,6 +58,17 @@ module Ask
|
|
|
58
58
|
@redis.call("DEL", *keys) if keys.any?
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def exists?(key)
|
|
62
|
+
@redis.call("EXISTS", prefixed(key)) == 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def keys(pattern: nil)
|
|
66
|
+
glob = pattern ? "#{NAMESPACE}:#{pattern}" : "#{NAMESPACE}:*"
|
|
67
|
+
raw = @redis.call("KEYS", glob)
|
|
68
|
+
prefix_len = NAMESPACE.length + 1
|
|
69
|
+
raw.map { |k| k[prefix_len..] }
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
# -- distributed locking --
|
|
62
73
|
|
|
63
74
|
def acquire_lock(key, ttl: 10)
|
|
@@ -117,7 +128,6 @@ module Ask
|
|
|
117
128
|
@redis.call("RPUSH", prefixed("list:#{key}"), JSON.generate(value))
|
|
118
129
|
return unless max_length
|
|
119
130
|
|
|
120
|
-
# LTRIM keeps only the specified range (negative indices, so -max_length means keep newest max_length)
|
|
121
131
|
@redis.call("LTRIM", prefixed("list:#{key}"), -max_length, -1)
|
|
122
132
|
end
|
|
123
133
|
|
|
@@ -13,6 +13,8 @@ module Ask
|
|
|
13
13
|
# key-value storage, distributed locking, message queues, and ordered
|
|
14
14
|
# lists. Uses the +sqlite3+ gem.
|
|
15
15
|
#
|
|
16
|
+
# Thread-safe via internal Mutex.
|
|
17
|
+
#
|
|
16
18
|
# @example
|
|
17
19
|
# store = Ask::State::Providers::SQLite.new(path: "sessions.db")
|
|
18
20
|
# store.set("key", { hello: "world" })
|
|
@@ -23,6 +25,7 @@ module Ask
|
|
|
23
25
|
def initialize(path: "sessions.db", **pragmas)
|
|
24
26
|
require "sqlite3"
|
|
25
27
|
|
|
28
|
+
@mutex = Mutex.new
|
|
26
29
|
@db = SQLite3::Database.new(path)
|
|
27
30
|
@db.results_as_hash = true
|
|
28
31
|
@db.busy_timeout = 5000
|
|
@@ -44,181 +47,235 @@ module Ask
|
|
|
44
47
|
# -- key-value --
|
|
45
48
|
|
|
46
49
|
def get(key)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
row = @db.get_first_row(<<~SQL, [key, Time.now.to_f])
|
|
52
|
+
SELECT value FROM state_store
|
|
53
|
+
WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
54
|
+
SQL
|
|
55
|
+
row ? JSON.parse(row["value"]) : nil
|
|
56
|
+
end
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
def set(key, value, ttl: nil)
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
@db.execute(<<~SQL, [key, JSON.generate(value), ttl ? Time.now.to_f + ttl : nil])
|
|
62
|
+
INSERT OR REPLACE INTO state_store (key, value, expires_at)
|
|
63
|
+
VALUES (?, ?, ?)
|
|
64
|
+
SQL
|
|
65
|
+
end
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def delete(key)
|
|
62
|
-
@
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@db.execute("DELETE FROM state_store WHERE key = ?", [key])
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
def set_if_not_exists(key, value, ttl: nil)
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
now = Time.now.to_f
|
|
77
|
+
expires = ttl ? now + ttl : nil
|
|
68
78
|
|
|
69
|
-
@db.transaction do
|
|
70
79
|
row = @db.get_first_row(
|
|
71
80
|
"SELECT 1 FROM state_store WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)",
|
|
72
81
|
[key, now]
|
|
73
82
|
)
|
|
74
|
-
|
|
83
|
+
return false if row
|
|
75
84
|
|
|
85
|
+
# Key doesn't exist or is expired — delete any leftovers, then insert
|
|
76
86
|
@db.execute("DELETE FROM state_store WHERE key = ?", [key])
|
|
77
87
|
@db.execute(<<~SQL, [key, JSON.generate(value), expires])
|
|
78
88
|
INSERT INTO state_store (key, value, expires_at)
|
|
79
89
|
VALUES (?, ?, ?)
|
|
80
90
|
SQL
|
|
81
|
-
|
|
91
|
+
true
|
|
82
92
|
end
|
|
83
93
|
end
|
|
84
94
|
|
|
85
95
|
def clear
|
|
86
|
-
@
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
@db.execute("DELETE FROM state_store")
|
|
98
|
+
@db.execute("DELETE FROM locks")
|
|
99
|
+
@db.execute("DELETE FROM queues")
|
|
100
|
+
@db.execute("DELETE FROM lists")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def exists?(key)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
row = @db.get_first_row(<<~SQL, [key, Time.now.to_f])
|
|
107
|
+
SELECT 1 FROM state_store
|
|
108
|
+
WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
109
|
+
SQL
|
|
110
|
+
!row.nil?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def keys(pattern: nil)
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
now = Time.now.to_f
|
|
117
|
+
sql, params = if pattern
|
|
118
|
+
like = self.class.glob_to_like(pattern)
|
|
119
|
+
[<<~SQL, [like, now]]
|
|
120
|
+
SELECT key FROM state_store
|
|
121
|
+
WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > ?)
|
|
122
|
+
SQL
|
|
123
|
+
else
|
|
124
|
+
[<<~SQL, [now]]
|
|
125
|
+
SELECT key FROM state_store
|
|
126
|
+
WHERE (expires_at IS NULL OR expires_at > ?)
|
|
127
|
+
SQL
|
|
128
|
+
end
|
|
129
|
+
@db.execute(sql, params).map { |r| r["key"] }
|
|
130
|
+
end
|
|
87
131
|
end
|
|
88
132
|
|
|
89
133
|
# -- distributed locking --
|
|
90
134
|
|
|
91
135
|
def acquire_lock(key, ttl: 10)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
136
|
+
@mutex.synchronize do
|
|
137
|
+
now = Time.now.to_f
|
|
138
|
+
expires_at_time = Time.now + ttl
|
|
139
|
+
token = SecureRandom.hex(16)
|
|
95
140
|
|
|
96
|
-
acquired = @db.transaction do
|
|
97
141
|
row = @db.get_first_row(
|
|
98
142
|
"SELECT 1 FROM locks WHERE key = ? AND expires_at > ?",
|
|
99
143
|
[key, now]
|
|
100
144
|
)
|
|
101
|
-
|
|
145
|
+
return nil if row
|
|
102
146
|
|
|
103
147
|
@db.execute("DELETE FROM locks WHERE key = ?", [key])
|
|
104
148
|
@db.execute(<<~SQL, [key, expires_at_time.to_f, token])
|
|
105
149
|
INSERT INTO locks (key, expires_at, token)
|
|
106
150
|
VALUES (?, ?, ?)
|
|
107
151
|
SQL
|
|
108
|
-
next true
|
|
109
|
-
end
|
|
110
152
|
|
|
111
|
-
|
|
153
|
+
Lock.new(id: key, token: token, expires_at: expires_at_time)
|
|
154
|
+
end
|
|
112
155
|
end
|
|
113
156
|
|
|
114
157
|
def release_lock(key, lock)
|
|
115
|
-
@
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
158
|
+
@mutex.synchronize do
|
|
159
|
+
@db.execute(
|
|
160
|
+
"DELETE FROM locks WHERE key = ? AND token = ?",
|
|
161
|
+
[key, lock.token]
|
|
162
|
+
)
|
|
163
|
+
@db.changes > 0
|
|
164
|
+
end
|
|
120
165
|
end
|
|
121
166
|
|
|
122
167
|
# -- message queues --
|
|
123
168
|
|
|
124
169
|
def enqueue(queue, value)
|
|
125
|
-
@
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
170
|
+
@mutex.synchronize do
|
|
171
|
+
@db.execute(<<~SQL, [queue, JSON.generate(value), Time.now.iso8601])
|
|
172
|
+
INSERT INTO queues (queue_name, value, enqueued_at)
|
|
173
|
+
VALUES (?, ?, ?)
|
|
174
|
+
SQL
|
|
175
|
+
id = @db.last_insert_row_id
|
|
176
|
+
QueueEntry.new(id: id.to_s, value: value, enqueued_at: Time.now)
|
|
177
|
+
end
|
|
131
178
|
end
|
|
132
179
|
|
|
133
180
|
def dequeue(queue)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
181
|
+
@mutex.synchronize do
|
|
182
|
+
row = @db.get_first_row(<<~SQL, [queue])
|
|
183
|
+
DELETE FROM queues
|
|
184
|
+
WHERE id = (
|
|
185
|
+
SELECT id FROM queues
|
|
186
|
+
WHERE queue_name = ?
|
|
187
|
+
ORDER BY id ASC
|
|
188
|
+
LIMIT 1
|
|
189
|
+
)
|
|
190
|
+
RETURNING id, value, enqueued_at
|
|
191
|
+
SQL
|
|
192
|
+
return nil unless row
|
|
145
193
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
QueueEntry.new(
|
|
195
|
+
id: row["id"].to_s,
|
|
196
|
+
value: JSON.parse(row["value"]),
|
|
197
|
+
enqueued_at: Time.parse(row["enqueued_at"])
|
|
198
|
+
)
|
|
199
|
+
end
|
|
151
200
|
end
|
|
152
201
|
|
|
153
202
|
def queue_depth(queue)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
@mutex.synchronize do
|
|
204
|
+
row = @db.get_first_row(
|
|
205
|
+
"SELECT COUNT(*) AS cnt FROM queues WHERE queue_name = ?", [queue]
|
|
206
|
+
)
|
|
207
|
+
row["cnt"]
|
|
208
|
+
end
|
|
158
209
|
end
|
|
159
210
|
|
|
160
211
|
# -- ordered lists --
|
|
161
212
|
|
|
162
213
|
def list_append(key, value, max_length: nil)
|
|
163
|
-
|
|
214
|
+
@mutex.synchronize do
|
|
215
|
+
serialized = JSON.generate(value)
|
|
164
216
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
@db.execute(<<~SQL, [key, serialized])
|
|
218
|
+
INSERT INTO lists (list_key, value)
|
|
219
|
+
VALUES (?, ?)
|
|
220
|
+
SQL
|
|
169
221
|
|
|
170
|
-
|
|
222
|
+
return unless max_length
|
|
171
223
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
SQL
|
|
182
|
-
return unless row && row["cutoff"]
|
|
224
|
+
row = @db.get_first_row(<<~SQL, [key, max_length])
|
|
225
|
+
SELECT MIN(id) AS cutoff FROM (
|
|
226
|
+
SELECT id FROM lists
|
|
227
|
+
WHERE list_key = ?
|
|
228
|
+
ORDER BY id DESC
|
|
229
|
+
LIMIT ?
|
|
230
|
+
)
|
|
231
|
+
SQL
|
|
232
|
+
return unless row && row["cutoff"]
|
|
183
233
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
234
|
+
@db.execute(<<~SQL, [key, row["cutoff"]])
|
|
235
|
+
DELETE FROM lists WHERE list_key = ? AND id < ?
|
|
236
|
+
SQL
|
|
237
|
+
end
|
|
187
238
|
end
|
|
188
239
|
|
|
189
240
|
def list_range(key, start = 0, stop = -1)
|
|
190
|
-
|
|
191
|
-
rows =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
@mutex.synchronize do
|
|
242
|
+
rows = if stop == -1
|
|
243
|
+
@db.execute(<<~SQL, [key, start])
|
|
244
|
+
SELECT value FROM lists
|
|
245
|
+
WHERE list_key = ?
|
|
246
|
+
ORDER BY id ASC
|
|
247
|
+
LIMIT -1 OFFSET ?
|
|
248
|
+
SQL
|
|
249
|
+
else
|
|
250
|
+
limit = stop - start + 1
|
|
251
|
+
@db.execute(<<~SQL, [key, limit, start])
|
|
252
|
+
SELECT value FROM lists
|
|
253
|
+
WHERE list_key = ?
|
|
254
|
+
ORDER BY id ASC
|
|
255
|
+
LIMIT ? OFFSET ?
|
|
256
|
+
SQL
|
|
257
|
+
end
|
|
258
|
+
rows.map { |r| JSON.parse(r["value"]) }
|
|
205
259
|
end
|
|
206
|
-
rows.map { |r| JSON.parse(r["value"]) }
|
|
207
260
|
end
|
|
208
261
|
|
|
209
262
|
def list_remove(key, value)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
@mutex.synchronize do
|
|
264
|
+
serialized = JSON.generate(value)
|
|
265
|
+
@db.execute(
|
|
266
|
+
"DELETE FROM lists WHERE list_key = ? AND value = ?",
|
|
267
|
+
[key, serialized]
|
|
268
|
+
)
|
|
269
|
+
@db.changes
|
|
270
|
+
end
|
|
216
271
|
end
|
|
217
272
|
|
|
218
273
|
# -- lifecycle --
|
|
219
274
|
|
|
220
275
|
def close
|
|
221
|
-
@
|
|
276
|
+
@mutex.synchronize do
|
|
277
|
+
@db.close
|
|
278
|
+
end
|
|
222
279
|
end
|
|
223
280
|
|
|
224
281
|
private
|
data/lib/ask-state-providers.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-state-providers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- LICENSE
|
|
80
80
|
- README.md
|
|
81
81
|
- lib/ask-state-providers.rb
|
|
82
|
+
- lib/ask/state/memory.rb
|
|
82
83
|
- lib/ask/state/providers/mysql.rb
|
|
83
84
|
- lib/ask/state/providers/postgres.rb
|
|
84
85
|
- lib/ask/state/providers/redis.rb
|