chat_sdk-state-mysql 1.0.1 → 1.2.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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chat_sdk/state/mysql.rb +133 -15
  3. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 444aca1b1505ff474ef9c3e52ec2287fb120c20c1e141762347e62dfd50cee7a
4
- data.tar.gz: ee821cebf2ec30e940c8d9e64495fe71f28323628eb58a562c25e17eb3a7cc92
3
+ metadata.gz: c3defa38d43dccd1b7baeb73f8e0000a4ef0db31748c967a85b5d327b1c73cf3
4
+ data.tar.gz: 16bfde09613137f441d82a8e7720eb2515307569f875b7d542120e4144c2787b
5
5
  SHA512:
6
- metadata.gz: e25a6f3318b9a40f2b2d03816e0e600916bee039eedb09c594c21502a62af1a346e51d9cee89ab9160580aa708abccd7cda8c63d90399d21f3b336a56dd29eec
7
- data.tar.gz: 582293dd5c2675748260e3d2242fdfb88b620f7d51fcccc999d14fa30a0b20b8713a98c12ebc2e75524964cd4e4ecbfc85605cc5df7ca623d616ed0d50bad206
6
+ metadata.gz: 6ba996e06d497b68d5aa63254fb66c16f61c1b663b609952ce89e36276b4ef8ac909a070e13b433181b288be372985cda0faa58a47381d211d8223bf997cd98c
7
+ data.tar.gz: 97699fbd0e4063ed56b1eff2c12e05d1825cf089ef8dcd23414345b414d9eb849c43391e6ed268915b3bb201e71ce2b937b8665950a1b5473bd19213032bdde4
@@ -3,10 +3,37 @@
3
3
  require "chat_sdk"
4
4
  require "mysql2"
5
5
  require "json"
6
+ require "uri"
6
7
 
7
8
  module ChatSDK
8
9
  module State
9
10
  class Mysql < Base
11
+ def self.parse_url(url)
12
+ uri = URI.parse(url)
13
+ options = {}
14
+ options[:host] = uri.host if uri.host
15
+ options[:port] = uri.port if uri.port
16
+ options[:username] = URI::DEFAULT_PARSER.unescape(uri.user) if uri.user
17
+ options[:password] = URI::DEFAULT_PARSER.unescape(uri.password) if uri.password
18
+ database = uri.path.delete_prefix("/")
19
+ options[:database] = URI::DEFAULT_PARSER.unescape(database) unless database.empty?
20
+ query_options = URI.decode_www_form(uri.query.to_s).to_h
21
+ query_options.transform_values! do |value|
22
+ case value
23
+ when /\A\d+\z/
24
+ value.to_i
25
+ when "true"
26
+ true
27
+ when "false"
28
+ false
29
+ else
30
+ value
31
+ end
32
+ end
33
+ options.merge!(query_options.transform_keys(&:to_sym))
34
+ options
35
+ end
36
+
10
37
  def initialize(url: nil, connection: nil, key_prefix: "chat_sdk", auto_migrate: true)
11
38
  @conn = connection || connect(url || ENV["MYSQL_URL"] || ENV["DATABASE_URL"] || "mysql2://root@localhost/chat_sdk_dev")
12
39
  @key_prefix = key_prefix
@@ -44,15 +71,15 @@ module ChatSDK
44
71
  def acquire_lock(key, owner:, ttl:)
45
72
  ttl_seconds = ttl.to_f
46
73
  del_stmt = @conn.prepare(
47
- "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ? AND expires_at < NOW()"
74
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ? AND expires_at < NOW(6)"
48
75
  )
49
76
  del_stmt.execute(@key_prefix, key)
50
77
  ins_stmt = @conn.prepare(
51
78
  "INSERT IGNORE INTO chat_sdk_locks (key_prefix, lock_key, owner, expires_at) " \
52
- "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl_seconds} SECOND))"
79
+ "VALUES (?, ?, ?, DATE_ADD(NOW(6), INTERVAL #{ttl_seconds} SECOND))"
53
80
  )
54
81
  ins_stmt.execute(@key_prefix, key, owner)
55
- @conn.affected_rows > 0
82
+ ins_stmt.affected_rows > 0
56
83
  end
57
84
 
58
85
  def release_lock(key, owner:)
@@ -60,25 +87,34 @@ module ChatSDK
60
87
  "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ? AND owner = ?"
61
88
  )
62
89
  stmt.execute(@key_prefix, key, owner)
63
- @conn.affected_rows > 0
90
+ stmt.affected_rows > 0
64
91
  end
65
92
 
66
93
  def force_lock(key, owner:, ttl:)
67
94
  ttl_seconds = ttl.to_f
68
95
  stmt = @conn.prepare(
69
96
  "INSERT INTO chat_sdk_locks (key_prefix, lock_key, owner, expires_at) " \
70
- "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl_seconds} SECOND)) " \
97
+ "VALUES (?, ?, ?, DATE_ADD(NOW(6), INTERVAL #{ttl_seconds} SECOND)) " \
71
98
  "ON DUPLICATE KEY UPDATE owner = VALUES(owner), expires_at = VALUES(expires_at)"
72
99
  )
73
100
  stmt.execute(@key_prefix, key, owner)
74
101
  true
75
102
  end
76
103
 
104
+ def extend_lock(key, owner:, ttl:)
105
+ stmt = @conn.prepare(
106
+ "UPDATE chat_sdk_locks SET expires_at = DATE_ADD(NOW(6), INTERVAL #{ttl.to_f} SECOND) " \
107
+ "WHERE key_prefix = ? AND lock_key = ? AND owner = ? AND expires_at > NOW(6)"
108
+ )
109
+ stmt.execute(@key_prefix, key, owner)
110
+ stmt.affected_rows > 0
111
+ end
112
+
77
113
  # Key-value store
78
114
 
79
115
  def get(key)
80
116
  stmt = @conn.prepare(
81
- "SELECT value FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())"
117
+ "SELECT value FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND (expires_at IS NULL OR expires_at > NOW(6))"
82
118
  )
83
119
  result = stmt.execute(@key_prefix, key)
84
120
  row = result.first
@@ -92,7 +128,7 @@ module ChatSDK
92
128
  stmt = if ttl
93
129
  @conn.prepare(
94
130
  "INSERT INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
95
- "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl.to_f} SECOND)) " \
131
+ "VALUES (?, ?, ?, DATE_ADD(NOW(6), INTERVAL #{ttl.to_f} SECOND)) " \
96
132
  "ON DUPLICATE KEY UPDATE value = VALUES(value), expires_at = VALUES(expires_at)"
97
133
  )
98
134
  else
@@ -120,13 +156,13 @@ module ChatSDK
120
156
  def set_if_absent(key, value, ttl: nil)
121
157
  serialized = JSON.generate(value)
122
158
  del_stmt = @conn.prepare(
123
- "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND expires_at IS NOT NULL AND expires_at < NOW()"
159
+ "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND expires_at IS NOT NULL AND expires_at < NOW(6)"
124
160
  )
125
161
  del_stmt.execute(@key_prefix, key)
126
162
  stmt = if ttl
127
163
  @conn.prepare(
128
164
  "INSERT IGNORE INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
129
- "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl.to_f} SECOND))"
165
+ "VALUES (?, ?, ?, DATE_ADD(NOW(6), INTERVAL #{ttl.to_f} SECOND))"
130
166
  )
131
167
  else
132
168
  @conn.prepare(
@@ -135,7 +171,53 @@ module ChatSDK
135
171
  )
136
172
  end
137
173
  stmt.execute(@key_prefix, key, serialized)
138
- @conn.affected_rows > 0
174
+ stmt.affected_rows > 0
175
+ end
176
+
177
+ def enqueue(key, value, max_size:, drop: :drop_oldest)
178
+ @conn.query("START TRANSACTION")
179
+ count_stmt = @conn.prepare(
180
+ "SELECT id FROM chat_sdk_queues WHERE key_prefix = ? AND queue_key = ? ORDER BY id FOR UPDATE"
181
+ )
182
+ ids = count_stmt.execute(@key_prefix, key).map { |row| row["id"] }
183
+ if ids.length >= max_size && drop.to_sym == :drop_newest
184
+ @conn.query("COMMIT")
185
+ return ids.length
186
+ end
187
+
188
+ insert = @conn.prepare("INSERT INTO chat_sdk_queues (key_prefix, queue_key, value) VALUES (?, ?, ?)")
189
+ insert.execute(@key_prefix, key, JSON.generate(value))
190
+ overflow = ids.length + 1 - max_size
191
+ if overflow.positive?
192
+ placeholders = Array.new(overflow, "?").join(", ")
193
+ remove = @conn.prepare("DELETE FROM chat_sdk_queues WHERE id IN (#{placeholders})")
194
+ remove.execute(*ids.first(overflow))
195
+ end
196
+ @conn.query("COMMIT")
197
+ [ids.length + 1, max_size].min
198
+ rescue
199
+ @conn.query("ROLLBACK")
200
+ raise
201
+ end
202
+
203
+ def drain_queue(key)
204
+ @conn.query("START TRANSACTION")
205
+ select = @conn.prepare(
206
+ "SELECT id, value FROM chat_sdk_queues WHERE key_prefix = ? AND queue_key = ? ORDER BY id FOR UPDATE"
207
+ )
208
+ rows = select.execute(@key_prefix, key).to_a
209
+ delete = @conn.prepare("DELETE FROM chat_sdk_queues WHERE key_prefix = ? AND queue_key = ?")
210
+ delete.execute(@key_prefix, key)
211
+ @conn.query("COMMIT")
212
+ rows.map { |row| JSON.parse(row["value"]) }
213
+ rescue
214
+ @conn.query("ROLLBACK")
215
+ raise
216
+ end
217
+
218
+ def queue_depth(key)
219
+ stmt = @conn.prepare("SELECT COUNT(*) AS count FROM chat_sdk_queues WHERE key_prefix = ? AND queue_key = ?")
220
+ stmt.execute(@key_prefix, key).first["count"].to_i
139
221
  end
140
222
 
141
223
  # Cleanup
@@ -147,15 +229,17 @@ module ChatSDK
147
229
  s2.execute(@key_prefix)
148
230
  s3 = @conn.prepare("DELETE FROM chat_sdk_cache WHERE key_prefix = ?")
149
231
  s3.execute(@key_prefix)
232
+ s4 = @conn.prepare("DELETE FROM chat_sdk_queues WHERE key_prefix = ?")
233
+ s4.execute(@key_prefix)
150
234
  end
151
235
 
152
236
  def cleanup_expired
153
237
  s1 = @conn.prepare(
154
- "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND expires_at IS NOT NULL AND expires_at < NOW()"
238
+ "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND expires_at IS NOT NULL AND expires_at < NOW(6)"
155
239
  )
156
240
  s1.execute(@key_prefix)
157
241
  s2 = @conn.prepare(
158
- "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND expires_at < NOW()"
242
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND expires_at < NOW(6)"
159
243
  )
160
244
  s2.execute(@key_prefix)
161
245
  end
@@ -163,7 +247,7 @@ module ChatSDK
163
247
  private
164
248
 
165
249
  def connect(url)
166
- Mysql2::Client.new(url)
250
+ Mysql2::Client.new(self.class.parse_url(url))
167
251
  end
168
252
 
169
253
  def ensure_tables
@@ -180,7 +264,7 @@ module ChatSDK
180
264
  key_prefix VARCHAR(255) NOT NULL,
181
265
  lock_key VARCHAR(512) NOT NULL,
182
266
  owner VARCHAR(255) NOT NULL,
183
- expires_at TIMESTAMP NOT NULL,
267
+ expires_at TIMESTAMP(6) NOT NULL,
184
268
  PRIMARY KEY (key_prefix, lock_key)
185
269
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
186
270
  SQL
@@ -189,10 +273,44 @@ module ChatSDK
189
273
  key_prefix VARCHAR(255) NOT NULL,
190
274
  cache_key VARCHAR(512) NOT NULL,
191
275
  value JSON NOT NULL,
192
- expires_at TIMESTAMP NULL DEFAULT NULL,
276
+ expires_at TIMESTAMP(6) NULL DEFAULT NULL,
193
277
  PRIMARY KEY (key_prefix, cache_key)
194
278
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
195
279
  SQL
280
+ @conn.query(<<~SQL)
281
+ CREATE TABLE IF NOT EXISTS chat_sdk_queues (
282
+ id BIGINT NOT NULL AUTO_INCREMENT,
283
+ key_prefix VARCHAR(255) NOT NULL,
284
+ queue_key VARCHAR(512) NOT NULL,
285
+ value JSON NOT NULL,
286
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
287
+ PRIMARY KEY (id),
288
+ INDEX chat_sdk_queues_lookup (key_prefix, queue_key)
289
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
290
+ SQL
291
+ ensure_expiration_precision
292
+ end
293
+
294
+ def ensure_expiration_precision
295
+ columns = @conn.query(<<~SQL).each_with_object({}) do |row, result|
296
+ SELECT TABLE_NAME, DATETIME_PRECISION
297
+ FROM information_schema.columns
298
+ WHERE table_schema = DATABASE()
299
+ AND table_name IN ('chat_sdk_locks', 'chat_sdk_cache')
300
+ AND column_name = 'expires_at'
301
+ SQL
302
+ result[row["TABLE_NAME"]] = row["DATETIME_PRECISION"].to_i
303
+ end
304
+
305
+ definitions = {
306
+ "chat_sdk_locks" => "TIMESTAMP(6) NOT NULL",
307
+ "chat_sdk_cache" => "TIMESTAMP(6) NULL DEFAULT NULL"
308
+ }
309
+ definitions.each do |table_name, definition|
310
+ next if columns[table_name] == 6
311
+
312
+ @conn.query("ALTER TABLE #{table_name} MODIFY expires_at #{definition}")
313
+ end
196
314
  end
197
315
  end
198
316
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk-state-mysql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0.1'
18
+ version: '1.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0.1'
25
+ version: '1.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: mysql2
28
28
  requirement: !ruby/object:Gem::Requirement