legion-cache 1.3.22 → 1.4.2

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.
data/lib/legion/cache.rb CHANGED
@@ -10,33 +10,77 @@ require 'legion/cache/redis'
10
10
  require 'legion/cache/redis_hash'
11
11
  require 'legion/cache/memory'
12
12
  require 'legion/cache/local'
13
+ require 'legion/cache/async_writer'
14
+ require 'legion/cache/reconnector'
15
+ require 'concurrent'
13
16
  require 'legion/cache/helper'
14
17
 
15
18
  module Legion
16
19
  module Cache
17
20
  extend Legion::Logging::Helper
18
21
 
22
+ @async_writer = Legion::Cache::AsyncWriter.new(settings_key: :cache)
23
+ @connected = Concurrent::AtomicBoolean.new(false)
24
+ @using_local = Concurrent::AtomicBoolean.new(false)
25
+ @using_memory = Concurrent::AtomicBoolean.new(false)
26
+
19
27
  class << self
20
28
  include Legion::Logging::Helper
21
29
 
30
+ def enabled?
31
+ return true unless defined?(Legion::Settings)
32
+
33
+ Legion::Settings.dig(:cache, :enabled) != false
34
+ rescue StandardError => e
35
+ handle_exception(e, level: :warn, handled: true, operation: :cache_enabled)
36
+ true
37
+ end
38
+
22
39
  def connected?
23
- @connected == true
40
+ @connected&.true? || false
24
41
  end
25
42
 
26
43
  def driver_name
27
- return 'memory' if @using_memory
28
- return 'local' if @using_local
44
+ return 'memory' if using_memory?
45
+ return 'local' if using_local?
29
46
 
30
47
  @active_shared_driver || configured_shared_driver
31
48
  end
32
49
 
50
+ def stats
51
+ {
52
+ driver: driver_name,
53
+ servers: resolved_servers,
54
+ enabled: enabled?,
55
+ connected: connected?,
56
+ using_local: using_local?,
57
+ using_memory: using_memory?,
58
+ pool_size: safe_pool_size,
59
+ pool_available: safe_pool_available,
60
+ async_pool_size: async_writer_pool_size,
61
+ async_queue_depth: async_writer_queue_depth,
62
+ async_processed: async_writer_processed_count,
63
+ async_failed: async_writer_failed_count,
64
+ reconnect_attempts: reconnector_attempts,
65
+ uptime: uptime_seconds
66
+ }.freeze
67
+ rescue StandardError => e
68
+ handle_exception(e, level: :warn, handled: true, operation: :cache_stats)
69
+ { error: e.message }.freeze
70
+ end
71
+
33
72
  def setup(**)
73
+ return unless enabled?
34
74
  return Legion::Settings[:cache][:connected] = true if connected?
35
75
 
76
+ @setup_at = Time.now
77
+
78
+ async_writer.start
79
+
36
80
  if ENV['LEGION_MODE'] == 'lite'
37
81
  Legion::Cache::Memory.setup
38
- @using_memory = true
39
- @connected = true
82
+ @using_memory.make_true
83
+ @connected.make_true
40
84
  Legion::Settings[:cache][:connected] = true
41
85
  log.info 'Legion::Cache using in-memory adapter (lite mode)'
42
86
  return
@@ -49,15 +93,20 @@ module Legion
49
93
 
50
94
  def shutdown
51
95
  log.info 'Shutting down Legion::Cache'
52
- if @using_memory
96
+ # 1. Drain async writer FIRST (while pool is still alive)
97
+ async_writer.stop(timeout: configured_shutdown_timeout)
98
+ # 2. Stop reconnector
99
+ stop_reconnector
100
+ # 3. Now close pools
101
+ if using_memory?
53
102
  Legion::Cache::Memory.shutdown
54
103
  else
55
- close unless @using_local
104
+ close unless using_local?
56
105
  Legion::Cache::Local.shutdown if Legion::Cache::Local.connected?
57
106
  end
58
- @using_local = false
59
- @using_memory = false
60
- @connected = false
107
+ @using_local.make_false
108
+ @using_memory.make_false
109
+ @connected.make_false
61
110
  Legion::Settings[:cache][:connected] = false
62
111
  end
63
112
 
@@ -65,44 +114,53 @@ module Legion
65
114
  Legion::Cache::Local
66
115
  end
67
116
 
117
+ def pool
118
+ @client
119
+ end
120
+
68
121
  def using_local?
69
- @using_local == true
122
+ @using_local&.true? || false
70
123
  end
71
124
 
72
125
  def using_memory?
73
- @using_memory == true
126
+ @using_memory&.true? || false
74
127
  end
75
128
 
76
129
  def client(**opts)
77
130
  if ENV['LEGION_MODE'] == 'lite'
78
131
  Legion::Cache::Memory.setup unless Legion::Cache::Memory.connected?
79
- @using_memory = true
80
- @using_local = false
81
- @connected = true
132
+ @using_memory.make_true
133
+ @using_local.make_false
134
+ @connected.make_true
82
135
  @active_shared_driver = nil
83
136
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
84
137
  return Legion::Cache::Memory.client
85
138
  end
86
139
 
87
140
  configure_shared_adapter!(opts[:driver])
88
- @using_memory = false
89
- @using_local = false
141
+ @using_memory.make_false
142
+ @using_local.make_false
90
143
  result = super
91
- @connected = true
144
+ # super (Pool) sets @connected to a plain boolean; restore AtomicBoolean
145
+ @connected = Concurrent::AtomicBoolean.new(true)
92
146
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
93
147
  result
94
148
  rescue StandardError
95
- @connected = false
149
+ @connected = Concurrent::AtomicBoolean.new(false)
96
150
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
97
151
  raise
98
152
  end
99
153
 
100
154
  def get(key)
101
- return Legion::Cache::Memory.get(key) if @using_memory
102
- return Legion::Cache::Local.get(key) if @using_local
155
+ return Legion::Cache::Memory.get(key) if using_memory?
156
+ return Legion::Cache::Local.get(key) if using_local?
157
+ return Legion::Cache::Local.get(key) if failback_to_local?
103
158
 
104
159
  configure_shared_adapter!
105
160
  super
161
+ rescue StandardError => e
162
+ handle_exception(e, level: :warn, handled: true, operation: :cache_get, key: key)
163
+ nil
106
164
  end
107
165
 
108
166
  def phi_max_ttl
@@ -121,35 +179,70 @@ module Legion
121
179
  [ttl, max].min
122
180
  end
123
181
 
124
- def set(key, value, ttl = nil, **opts)
125
- ttl = opts.delete(:ttl) || ttl || 180
126
- effective_ttl = enforce_phi_ttl(ttl, **opts)
127
- return Legion::Cache::Memory.set(key, value, effective_ttl) if @using_memory
128
- return Legion::Cache::Local.set(key, value, effective_ttl) if @using_local
182
+ def set(key, value, ttl: nil, async: true, phi: false)
183
+ effective_ttl = resolve_ttl(ttl, phi: phi)
184
+
185
+ if async && async_writer.running?
186
+ async_writer.enqueue { set_internal(key, value, ttl: effective_ttl) }
187
+ true
188
+ else
189
+ set_internal(key, value, ttl: effective_ttl)
190
+ end
191
+ end
192
+
193
+ def set_nx(key, value, ttl: nil)
194
+ effective_ttl = resolve_ttl(ttl)
195
+ return Legion::Cache::Memory.set_nx(key, value, ttl: effective_ttl) if using_memory?
196
+ return Legion::Cache::Local.set_nx(key, value, ttl: effective_ttl) if using_local?
197
+ return Legion::Cache::Local.set_nx(key, value, ttl: effective_ttl) if failback_to_local?
129
198
 
130
199
  configure_shared_adapter!
131
- super(key, value, effective_ttl)
200
+ super
201
+ rescue StandardError => e
202
+ handle_exception(e, level: :warn, handled: true, operation: :cache_set_nx, key: key)
203
+ false
132
204
  end
133
205
 
134
- def fetch(key, ttl = nil, &)
135
- return Legion::Cache::Memory.fetch(key, ttl, &) if @using_memory
136
- return Legion::Cache::Local.fetch(key, ttl, &) if @using_local
206
+ def set_sync(key, value, ttl: nil, **)
207
+ return Legion::Cache::Memory.set_sync(key, value, ttl: ttl) if using_memory?
208
+ return Legion::Cache::Local.set_sync(key, value, ttl: ttl) if using_local?
209
+ return Legion::Cache::Local.set_sync(key, value, ttl: ttl) if failback_to_local?
137
210
 
138
211
  configure_shared_adapter!
139
212
  super
140
213
  end
141
214
 
142
- def delete(key)
143
- return Legion::Cache::Memory.delete(key) if @using_memory
144
- return Legion::Cache::Local.delete(key) if @using_local
215
+ def fetch(key, ttl: nil, &)
216
+ return Legion::Cache::Memory.fetch(key, ttl: ttl, &) if using_memory?
217
+ return Legion::Cache::Local.fetch(key, ttl: ttl, &) if using_local?
218
+ return Legion::Cache::Local.fetch(key, ttl: ttl, &) if failback_to_local?
145
219
 
146
220
  configure_shared_adapter!
147
221
  super
148
222
  end
149
223
 
150
- def flush(delay = 0)
151
- return Legion::Cache::Memory.flush(delay) if @using_memory
152
- return Legion::Cache::Local.flush(delay) if @using_local
224
+ def delete(key, async: true)
225
+ if async && async_writer.running?
226
+ async_writer.enqueue { delete_internal(key) }
227
+ true
228
+ else
229
+ delete_internal(key)
230
+ end
231
+ end
232
+
233
+ def delete_sync(key)
234
+ return Legion::Cache::Memory.delete_sync(key) if using_memory?
235
+ return Legion::Cache::Local.delete_sync(key) if using_local?
236
+ return Legion::Cache::Local.delete_sync(key) if failback_to_local?
237
+
238
+ configure_shared_adapter!
239
+ super
240
+ end
241
+
242
+ def flush
243
+ return Legion::Cache::Memory.flush if using_memory?
244
+ return Legion::Cache::Local.flush if using_local?
245
+ return Legion::Cache::Local.flush if failback_to_local?
153
246
 
154
247
  configure_shared_adapter!
155
248
  super
@@ -158,35 +251,48 @@ module Legion
158
251
  def mget(*keys)
159
252
  keys = keys.flatten
160
253
  return {} if keys.empty?
161
- return keys.to_h { |key| [key, Legion::Cache::Memory.get(key)] } if @using_memory
162
- return local_mget(*keys) if @using_local
254
+ return keys.to_h { |key| [key, Legion::Cache::Memory.get(key)] } if using_memory?
255
+ return Legion::Cache::Local.mget(*keys) if using_local?
256
+ return Legion::Cache::Local.mget(*keys) if failback_to_local?
163
257
 
164
258
  configure_shared_adapter!
165
259
  super
166
260
  end
167
261
 
168
- def mset(hash)
262
+ def mset(hash, ttl: nil, async: true)
169
263
  return true if hash.empty?
170
- return hash.each { |key, value| Legion::Cache::Memory.set(key, value) } && true if @using_memory
171
- return local_mset(hash) if @using_local
264
+
265
+ if async && async_writer.running?
266
+ async_writer.enqueue { mset_internal(hash, ttl: ttl) }
267
+ true
268
+ else
269
+ mset_internal(hash, ttl: ttl)
270
+ end
271
+ end
272
+
273
+ def mset_sync(hash, ttl: nil, **)
274
+ return true if hash.empty?
275
+ return hash.each { |key, value| Legion::Cache::Memory.set_sync(key, value, ttl: ttl) } && true if using_memory?
276
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if using_local?
277
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if failback_to_local?
172
278
 
173
279
  configure_shared_adapter!
174
280
  super
175
281
  end
176
282
 
177
283
  def close
178
- if @using_memory
284
+ if using_memory?
179
285
  Legion::Cache::Memory.shutdown
180
- @using_memory = false
181
- @connected = false
286
+ @using_memory.make_false
287
+ @connected.make_false
182
288
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
183
289
  return false
184
290
  end
185
291
 
186
- if @using_local
292
+ if using_local?
187
293
  Legion::Cache::Local.close
188
- @using_local = false
189
- @connected = false
294
+ @using_local.make_false
295
+ @connected.make_false
190
296
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
191
297
  return false
192
298
  end
@@ -195,48 +301,48 @@ module Legion
195
301
 
196
302
  configure_shared_adapter!
197
303
  result = super
198
- @connected = false
304
+ @connected = Concurrent::AtomicBoolean.new(false)
199
305
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
200
306
  result
201
307
  end
202
308
 
203
309
  def restart(**opts)
204
310
  configure_shared_adapter!(opts[:driver])
205
- @using_memory = false
206
- @using_local = false
311
+ @using_memory.make_false
312
+ @using_local.make_false
207
313
  result = super
208
- @connected = true
314
+ @connected = Concurrent::AtomicBoolean.new(true)
209
315
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
210
316
  result
211
317
  end
212
318
 
213
319
  def size
214
- return Legion::Cache::Memory.size if @using_memory
215
- return Legion::Cache::Local.size if @using_local
320
+ return Legion::Cache::Memory.size if using_memory?
321
+ return Legion::Cache::Local.size if using_local?
216
322
 
217
323
  configure_shared_adapter!
218
324
  super
219
325
  end
220
326
 
221
327
  def available
222
- return Legion::Cache::Memory.available if @using_memory
223
- return Legion::Cache::Local.available if @using_local
328
+ return Legion::Cache::Memory.available if using_memory?
329
+ return Legion::Cache::Local.available if using_local?
224
330
 
225
331
  configure_shared_adapter!
226
332
  super
227
333
  end
228
334
 
229
335
  def pool_size
230
- return Legion::Cache::Memory.size if @using_memory
231
- return Legion::Cache::Local.pool_size if @using_local
336
+ return Legion::Cache::Memory.size if using_memory?
337
+ return Legion::Cache::Local.pool_size if using_local?
232
338
 
233
339
  configure_shared_adapter!
234
340
  super
235
341
  end
236
342
 
237
343
  def timeout
238
- return 0 if @using_memory
239
- return Legion::Cache::Local.timeout if @using_local
344
+ return 0 if using_memory?
345
+ return Legion::Cache::Local.timeout if using_local?
240
346
 
241
347
  configure_shared_adapter!
242
348
  super
@@ -244,6 +350,64 @@ module Legion
244
350
 
245
351
  private
246
352
 
353
+ def async_writer
354
+ Legion::Cache.instance_variable_get(:@async_writer)
355
+ end
356
+
357
+ def set_internal(key, value, ttl: nil)
358
+ return Legion::Cache::Memory.set(key, value, ttl: ttl) if using_memory?
359
+ return Legion::Cache::Local.set(key, value, ttl: ttl) if using_local?
360
+ return Legion::Cache::Local.set(key, value, ttl: ttl) if failback_to_local?
361
+
362
+ configure_shared_adapter!
363
+ set_sync(key, value, ttl: ttl)
364
+ end
365
+
366
+ def delete_internal(key)
367
+ return Legion::Cache::Memory.delete(key) if using_memory?
368
+ return Legion::Cache::Local.delete(key) if using_local?
369
+ return Legion::Cache::Local.delete(key) if failback_to_local?
370
+
371
+ configure_shared_adapter!
372
+ delete_sync(key)
373
+ end
374
+
375
+ def mset_internal(hash, ttl: nil)
376
+ return hash.each { |key, value| Legion::Cache::Memory.set(key, value, ttl: ttl) } && true if using_memory?
377
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if using_local?
378
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if failback_to_local?
379
+
380
+ configure_shared_adapter!
381
+ mset_sync(hash, ttl: ttl)
382
+ end
383
+
384
+ def failback_to_local?
385
+ return false unless Legion::Cache::Local.connected?
386
+
387
+ setting = if defined?(Legion::Settings)
388
+ Legion::Settings.dig(:cache, :failback_to_local) != false
389
+ else
390
+ true
391
+ end
392
+ setting && (!enabled? || !connected?)
393
+ rescue StandardError => e
394
+ handle_exception(e, level: :warn, handled: true, operation: :cache_failback_check)
395
+ false
396
+ end
397
+
398
+ def resolve_ttl(ttl, phi: false)
399
+ effective = ttl || default_ttl
400
+ enforce_phi_ttl(effective, phi: phi)
401
+ end
402
+
403
+ def default_ttl
404
+ return 3600 unless defined?(Legion::Settings)
405
+
406
+ Legion::Settings.dig(:cache, :default_ttl) || 3600
407
+ rescue StandardError
408
+ 3600
409
+ end
410
+
247
411
  def setup_local
248
412
  return if Legion::Cache::Local.connected?
249
413
 
@@ -254,8 +418,8 @@ module Legion
254
418
 
255
419
  def setup_shared(**)
256
420
  client(**Legion::Settings[:cache], logger: log, **)
257
- @connected = true
258
- @using_local = false
421
+ @connected.make_true
422
+ @using_local.make_false
259
423
  Legion::Settings[:cache][:connected] = true
260
424
  driver = Legion::Settings[:cache][:driver] || 'dalli'
261
425
  servers = Array(Legion::Settings[:cache][:servers]).join(', ')
@@ -263,15 +427,24 @@ module Legion
263
427
  rescue StandardError => e
264
428
  report_exception(e, level: :warn, handled: true, operation: :setup_shared, fallback: :local)
265
429
  if Legion::Cache::Local.connected?
266
- @using_local = true
267
- @connected = true
430
+ @using_local.make_true
431
+ @connected.make_true
268
432
  Legion::Settings[:cache][:connected] = true
269
433
  log.info 'Legion::Cache fell back to Local cache'
270
434
  else
271
- @connected = false
435
+ @connected.make_false
272
436
  Legion::Settings[:cache][:connected] = false
273
437
  log.error 'Legion::Cache shared and local adapters are unavailable'
274
438
  end
439
+ start_reconnector if enabled?
440
+ end
441
+
442
+ def reconnect_shared!
443
+ client(**Legion::Settings[:cache], logger: log)
444
+ @connected.make_true
445
+ @using_local.make_false
446
+ Legion::Settings[:cache][:connected] = true
447
+ log.info 'Legion::Cache shared reconnected'
275
448
  end
276
449
 
277
450
  def report_exception(exception, level:, handled:, **)
@@ -317,16 +490,96 @@ module Legion
317
490
  handle_exception(e, level: :warn, handled: true, operation: :cache_close_existing_shared_client)
318
491
  ensure
319
492
  @client = nil
320
- @connected = false
493
+ @connected = Concurrent::AtomicBoolean.new(false)
321
494
  end
322
495
 
323
- def local_mget(*keys)
324
- keys.to_h { |key| [key, Legion::Cache::Local.get(key)] }
496
+ def resolved_servers
497
+ return [] if using_memory?
498
+
499
+ Array(Legion::Settings.dig(:cache, :servers))
500
+ rescue StandardError
501
+ []
325
502
  end
326
503
 
327
- def local_mset(hash)
328
- hash.each { |key, value| Legion::Cache::Local.set(key, value) }
329
- true
504
+ def safe_pool_size
505
+ return 1 if using_memory?
506
+ return 0 unless connected?
507
+
508
+ pool_size
509
+ rescue StandardError
510
+ 0
511
+ end
512
+
513
+ def safe_pool_available
514
+ return 1 if using_memory?
515
+ return 0 unless connected?
516
+
517
+ available
518
+ rescue StandardError
519
+ 0
520
+ end
521
+
522
+ def async_writer_pool_size
523
+ async_writer.pool_size
524
+ rescue StandardError
525
+ 0
526
+ end
527
+
528
+ def async_writer_queue_depth
529
+ async_writer.queue_depth
530
+ rescue StandardError
531
+ 0
532
+ end
533
+
534
+ def async_writer_processed_count
535
+ async_writer.processed_count
536
+ rescue StandardError
537
+ 0
538
+ end
539
+
540
+ def configured_shutdown_timeout
541
+ return 5 unless defined?(Legion::Settings)
542
+
543
+ Legion::Settings.dig(:cache, :async, :shutdown_timeout) || 5
544
+ rescue StandardError
545
+ 5
546
+ end
547
+
548
+ def async_writer_failed_count
549
+ async_writer.failed_count
550
+ rescue StandardError
551
+ 0
552
+ end
553
+
554
+ def reconnector_attempts
555
+ @reconnector&.attempts || 0
556
+ end
557
+
558
+ def start_reconnector
559
+ return unless enabled?
560
+
561
+ stop_reconnector
562
+ @reconnector = Legion::Cache::Reconnector.new(
563
+ tier: :shared,
564
+ connect_block: -> { reconnect_shared! },
565
+ enabled_block: -> { enabled? },
566
+ settings_key: :cache
567
+ )
568
+ @reconnector.start
569
+ log.info 'Legion::Cache started background reconnector for shared tier'
570
+ end
571
+
572
+ def stop_reconnector
573
+ @reconnector&.stop
574
+ @reconnector = nil
575
+ end
576
+
577
+ def uptime_seconds
578
+ return 0 unless @setup_at
579
+
580
+ (Time.now - @setup_at).to_i
581
+ rescue StandardError
582
+ 0
330
583
  end
331
584
  end
332
585
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.22
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.2'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: connection_pool
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -102,12 +116,14 @@ files:
102
116
  - README.md
103
117
  - legion-cache.gemspec
104
118
  - lib/legion/cache.rb
119
+ - lib/legion/cache/async_writer.rb
105
120
  - lib/legion/cache/cacheable.rb
106
121
  - lib/legion/cache/helper.rb
107
122
  - lib/legion/cache/local.rb
108
123
  - lib/legion/cache/memcached.rb
109
124
  - lib/legion/cache/memory.rb
110
125
  - lib/legion/cache/pool.rb
126
+ - lib/legion/cache/reconnector.rb
111
127
  - lib/legion/cache/redis.rb
112
128
  - lib/legion/cache/redis_hash.rb
113
129
  - lib/legion/cache/settings.rb