familia 1.2.1 → 2.0.0.pre2
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/.github/workflows/ci.yml +68 -0
- data/.github/workflows/docs.yml +64 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +3 -1
- data/.rubocop.yml +16 -9
- data/.rubocop_todo.yml +177 -31
- data/.yardopts +9 -0
- data/CLAUDE.md +141 -0
- data/Gemfile +16 -2
- data/Gemfile.lock +97 -36
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +192 -0
- data/familia.gemspec +10 -6
- data/lib/familia/base.rb +19 -9
- data/lib/familia/connection.rb +232 -65
- data/lib/familia/core_ext.rb +1 -1
- data/lib/familia/datatype/commands.rb +59 -0
- data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
- data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
- data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
- data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
- data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
- data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
- data/lib/familia/datatype.rb +243 -0
- data/lib/familia/errors.rb +5 -2
- data/lib/familia/features/expiration.rb +33 -34
- data/lib/familia/features/quantization.rb +9 -3
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features.rb +2 -2
- data/lib/familia/horreum/class_methods.rb +97 -110
- data/lib/familia/horreum/commands.rb +46 -51
- data/lib/familia/horreum/connection.rb +82 -0
- data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
- data/lib/familia/horreum/serialization.rb +61 -198
- data/lib/familia/horreum/settings.rb +6 -17
- data/lib/familia/horreum/utils.rb +11 -10
- data/lib/familia/horreum.rb +69 -60
- data/lib/familia/logging.rb +12 -12
- data/lib/familia/multi_result.rb +72 -0
- data/lib/familia/refinements.rb +7 -44
- data/lib/familia/settings.rb +11 -11
- data/lib/familia/utils.rb +123 -90
- data/lib/familia/version.rb +4 -21
- data/lib/familia.rb +18 -13
- data/lib/middleware/database_middleware.rb +150 -0
- data/try/configuration/scenarios_try.rb +65 -0
- data/try/core/connection_try.rb +58 -0
- data/try/core/errors_try.rb +93 -0
- data/try/core/extensions_try.rb +26 -0
- data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
- data/try/{00_familia_try.rb → core/familia_try.rb} +7 -5
- data/try/core/middleware_try.rb +68 -0
- data/try/core/refinements_try.rb +39 -0
- data/try/core/settings_try.rb +76 -0
- data/try/core/tools_try.rb +54 -0
- data/try/core/utils_try.rb +189 -0
- data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
- data/try/datatypes/datatype_base_try.rb +69 -0
- data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
- data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
- data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
- data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
- data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
- data/try/edge_cases/empty_identifiers_try.rb +48 -0
- data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
- data/try/edge_cases/json_serialization_try.rb +85 -0
- data/try/edge_cases/race_conditions_try.rb +60 -0
- data/try/edge_cases/reserved_keywords_try.rb +59 -0
- data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
- data/try/edge_cases/ttl_side_effects_try.rb +51 -0
- data/try/features/expiration_try.rb +86 -0
- data/try/features/quantization_try.rb +90 -0
- data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
- data/try/features/safe_dump_try.rb +137 -0
- data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
- data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
- data/try/horreum/class_methods_try.rb +41 -0
- data/try/horreum/commands_try.rb +49 -0
- data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
- data/try/horreum/relations_try.rb +146 -0
- data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
- data/try/horreum/settings_try.rb +43 -0
- data/try/integration/cross_component_try.rb +46 -0
- data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
- data/try/{40_customer_try.rb → models/customer_try.rb} +21 -18
- data/try/models/datatype_base_try.rb +100 -0
- data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
- data/try/performance/benchmarks_try.rb +55 -0
- data/try/pooling/README.md +20 -0
- data/try/pooling/configurable_stress_test_try.rb +435 -0
- data/try/pooling/connection_pool_test_try.rb +273 -0
- data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- data/try/pooling/lib/connection_pool_metrics.rb +372 -0
- data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
- data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
- data/try/pooling/lib/visualize_stress_results.rb +434 -0
- data/try/pooling/pool_siege_try.rb +509 -0
- data/try/pooling/run_stress_tests_try.rb +482 -0
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
- data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
- data/try/prototypes/atomic_saves_v4.rb +105 -0
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- metadata +143 -46
- data/.github/workflows/ruby.yml +0 -71
- data/VERSION.yml +0 -4
- data/lib/familia/redistype/commands.rb +0 -59
- data/lib/familia/redistype.rb +0 -228
- data/lib/familia/tools.rb +0 -68
- data/lib/redis_middleware.rb +0 -109
- data/try/20_redis_type_try.rb +0 -70
- data/try/91_json_bug_try.rb +0 -86
data/lib/familia/redistype.rb
DELETED
@@ -1,228 +0,0 @@
|
|
1
|
-
# rubocop:disable all
|
2
|
-
|
3
|
-
require_relative 'redistype/commands'
|
4
|
-
require_relative 'redistype/serialization'
|
5
|
-
|
6
|
-
module Familia
|
7
|
-
|
8
|
-
# RedisType - Base class for Redis data type wrappers
|
9
|
-
#
|
10
|
-
# This class provides common functionality for various Redis data types
|
11
|
-
# such as String, List, Set, SortedSet, and HashKey.
|
12
|
-
#
|
13
|
-
# @abstract Subclass and implement Redis data type specific methods
|
14
|
-
class RedisType
|
15
|
-
include Familia::Base
|
16
|
-
extend Familia::Features
|
17
|
-
|
18
|
-
@registered_types = {}
|
19
|
-
@valid_options = %i[class parent ttl default db key redis suffix prefix]
|
20
|
-
@db = nil
|
21
|
-
|
22
|
-
feature :expiration
|
23
|
-
feature :quantization
|
24
|
-
|
25
|
-
class << self
|
26
|
-
attr_reader :registered_types, :valid_options, :has_relations
|
27
|
-
attr_accessor :parent
|
28
|
-
attr_writer :db, :uri
|
29
|
-
end
|
30
|
-
|
31
|
-
module ClassMethods
|
32
|
-
# To be called inside every class that inherits RedisType
|
33
|
-
# +methname+ is the term used for the class and instance methods
|
34
|
-
# that are created for the given +klass+ (e.g. set, list, etc)
|
35
|
-
def register(klass, methname)
|
36
|
-
Familia.ld "[#{self}] Registering #{klass} as #{methname.inspect}"
|
37
|
-
|
38
|
-
@registered_types[methname] = klass
|
39
|
-
end
|
40
|
-
|
41
|
-
def db(val = nil)
|
42
|
-
@db = val unless val.nil?
|
43
|
-
@db || parent&.db
|
44
|
-
end
|
45
|
-
|
46
|
-
def uri(val = nil)
|
47
|
-
@uri = val unless val.nil?
|
48
|
-
@uri || (parent ? parent.uri : Familia.uri)
|
49
|
-
end
|
50
|
-
|
51
|
-
def inherited(obj)
|
52
|
-
Familia.trace :REDISTYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
|
53
|
-
obj.db = db
|
54
|
-
obj.ttl = ttl # method added via Features::Expiration
|
55
|
-
obj.uri = uri
|
56
|
-
obj.parent = self
|
57
|
-
super(obj)
|
58
|
-
end
|
59
|
-
|
60
|
-
def valid_keys_only(opts)
|
61
|
-
opts.select { |k, _| RedisType.valid_options.include? k }
|
62
|
-
end
|
63
|
-
|
64
|
-
def has_relations?
|
65
|
-
@has_relations ||= false
|
66
|
-
end
|
67
|
-
end
|
68
|
-
extend ClassMethods
|
69
|
-
|
70
|
-
attr_reader :keystring, :parent, :opts
|
71
|
-
attr_writer :dump_method, :load_method
|
72
|
-
|
73
|
-
# +keystring+: If parent is set, this will be used as the suffix
|
74
|
-
# for rediskey. Otherwise this becomes the value of the key.
|
75
|
-
# If this is an Array, the elements will be joined.
|
76
|
-
#
|
77
|
-
# Options:
|
78
|
-
#
|
79
|
-
# :class => A class that responds to Familia.load_method and
|
80
|
-
# Familia.dump_method. These will be used when loading and
|
81
|
-
# saving data from/to redis to unmarshal/marshal the class.
|
82
|
-
#
|
83
|
-
# :parent => The Familia object that this redistype object belongs
|
84
|
-
# to. This can be a class that includes Familia or an instance.
|
85
|
-
#
|
86
|
-
# :ttl => the time to live in seconds. When not nil, this will
|
87
|
-
# set the redis expire for this key whenever #save is called.
|
88
|
-
# You can also call it explicitly via #update_expiration.
|
89
|
-
#
|
90
|
-
# :default => the default value (String-only)
|
91
|
-
#
|
92
|
-
# :db => the redis database to use (ignored if :redis is used).
|
93
|
-
#
|
94
|
-
# :redis => an instance of Redis.
|
95
|
-
#
|
96
|
-
# :key => a hardcoded key to use instead of the deriving the from
|
97
|
-
# the name and parent (e.g. a derived key: customer:custid:secret_counter).
|
98
|
-
#
|
99
|
-
# :suffix => the suffix to use for the key (e.g. 'scores' in customer:custid:scores).
|
100
|
-
# :prefix => the prefix to use for the key (e.g. 'customer' in customer:custid:scores).
|
101
|
-
#
|
102
|
-
# Connection precendence: uses the redis connection of the parent or the
|
103
|
-
# value of opts[:redis] or Familia.redis (in that order).
|
104
|
-
def initialize(keystring, opts = {})
|
105
|
-
#Familia.ld " [initializing] #{self.class} #{opts}"
|
106
|
-
@keystring = keystring
|
107
|
-
@keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
|
108
|
-
|
109
|
-
# Remove all keys from the opts that are not in the allowed list
|
110
|
-
@opts = opts || {}
|
111
|
-
@opts = RedisType.valid_keys_only(@opts)
|
112
|
-
|
113
|
-
# Apply the options to instance method setters of the same name
|
114
|
-
@opts.each do |k, v|
|
115
|
-
# Bewarde logging :parent instance here implicitly calls #to_s which for
|
116
|
-
# some classes could include the identifier which could still be nil at
|
117
|
-
# this point. This would result in a Familia::Problem being raised. So
|
118
|
-
# to be on the safe-side here until we have a better understanding of
|
119
|
-
# the issue, we'll just log the class name for each key-value pair.
|
120
|
-
Familia.ld " [setting] #{k} #{v.class}"
|
121
|
-
send(:"#{k}=", v) if respond_to? :"#{k}="
|
122
|
-
end
|
123
|
-
|
124
|
-
init if respond_to? :init
|
125
|
-
end
|
126
|
-
|
127
|
-
def redis
|
128
|
-
return @redis if @redis
|
129
|
-
|
130
|
-
parent? ? parent.redis : Familia.redis(opts[:db])
|
131
|
-
end
|
132
|
-
|
133
|
-
# Produces the full Redis key for this object.
|
134
|
-
#
|
135
|
-
# @return [String] The full Redis key.
|
136
|
-
#
|
137
|
-
# This method determines the appropriate Redis key based on the context of the RedisType object:
|
138
|
-
#
|
139
|
-
# 1. If a hardcoded key is set in the options, it returns that key.
|
140
|
-
# 2. For instance-level RedisType objects, it uses the parent instance's rediskey method.
|
141
|
-
# 3. For class-level RedisType objects, it uses the parent class's rediskey method.
|
142
|
-
# 4. For standalone RedisType objects, it uses the keystring as the full Redis key.
|
143
|
-
#
|
144
|
-
# For class-level RedisType objects (parent_class? == true):
|
145
|
-
# - The suffix is optional and used to differentiate between different types of objects.
|
146
|
-
# - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
|
147
|
-
# - If a nil suffix is explicitly passed, it won't appear in the resulting Redis key.
|
148
|
-
# - Passing nil as the suffix is how class-level RedisType objects are created without
|
149
|
-
# the global default 'object' suffix.
|
150
|
-
#
|
151
|
-
# @example Instance-level RedisType
|
152
|
-
# user_instance.some_redistype.rediskey # => "user:123:some_redistype"
|
153
|
-
#
|
154
|
-
# @example Class-level RedisType
|
155
|
-
# User.some_redistype.rediskey # => "user:some_redistype"
|
156
|
-
#
|
157
|
-
# @example Standalone RedisType
|
158
|
-
# RedisType.new("mykey").rediskey # => "mykey"
|
159
|
-
#
|
160
|
-
# @example Class-level RedisType with explicit nil suffix
|
161
|
-
# User.rediskey("123", nil) # => "user:123"
|
162
|
-
#
|
163
|
-
def rediskey
|
164
|
-
# Return the hardcoded key if it's set. This is useful for
|
165
|
-
# support legacy keys that aren't derived in the same way.
|
166
|
-
return opts[:key] if opts[:key]
|
167
|
-
|
168
|
-
if parent_instance?
|
169
|
-
# This is an instance-level redistype object so the parent instance's
|
170
|
-
# rediskey method is defined in Familia::Horreum::InstanceMethods.
|
171
|
-
parent.rediskey(keystring)
|
172
|
-
elsif parent_class?
|
173
|
-
# This is a class-level redistype object so the parent class' rediskey
|
174
|
-
# method is defined in Familia::Horreum::ClassMethods.
|
175
|
-
parent.rediskey(keystring, nil)
|
176
|
-
else
|
177
|
-
# This is a standalone RedisType object where it's keystring
|
178
|
-
# is the full redis key.
|
179
|
-
keystring
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def class?
|
184
|
-
!@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
|
185
|
-
end
|
186
|
-
|
187
|
-
def parent_instance?
|
188
|
-
parent.is_a?(Familia::Horreum)
|
189
|
-
end
|
190
|
-
|
191
|
-
def parent_class?
|
192
|
-
parent.is_a?(Class) && parent <= Familia::Horreum
|
193
|
-
end
|
194
|
-
|
195
|
-
def parent?
|
196
|
-
parent_class? || parent_instance?
|
197
|
-
end
|
198
|
-
|
199
|
-
def parent
|
200
|
-
@opts[:parent]
|
201
|
-
end
|
202
|
-
|
203
|
-
def db
|
204
|
-
@opts[:db] || self.class.db
|
205
|
-
end
|
206
|
-
|
207
|
-
def uri
|
208
|
-
@opts[:uri] || self.class.uri
|
209
|
-
end
|
210
|
-
|
211
|
-
def dump_method
|
212
|
-
@dump_method || self.class.dump_method
|
213
|
-
end
|
214
|
-
|
215
|
-
def load_method
|
216
|
-
@load_method || self.class.load_method
|
217
|
-
end
|
218
|
-
|
219
|
-
include Commands
|
220
|
-
include Serialization
|
221
|
-
end
|
222
|
-
|
223
|
-
require_relative 'redistype/types/list'
|
224
|
-
require_relative 'redistype/types/unsorted_set'
|
225
|
-
require_relative 'redistype/types/sorted_set'
|
226
|
-
require_relative 'redistype/types/hashkey'
|
227
|
-
require_relative 'redistype/types/string'
|
228
|
-
end
|
data/lib/familia/tools.rb
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Familia
|
4
|
-
module Tools
|
5
|
-
extend self
|
6
|
-
def move_keys(filter, source_uri, target_uri, &each_key)
|
7
|
-
raise "Source and target are the same (#{target_uri})" if target_uri == source_uri
|
8
|
-
|
9
|
-
Familia.connect target_uri
|
10
|
-
source_keys = Familia.redis(source_uri).keys(filter)
|
11
|
-
puts "Moving #{source_keys.size} keys from #{source_uri} to #{target_uri} (filter: #{filter})"
|
12
|
-
source_keys.each_with_index do |key, idx|
|
13
|
-
type = Familia.redis(source_uri).type key
|
14
|
-
ttl = Familia.redis(source_uri).ttl key
|
15
|
-
if source_uri.host == target_uri.host && source_uri.port == target_uri.port
|
16
|
-
Familia.redis(source_uri).move key, target_uri.db
|
17
|
-
else
|
18
|
-
case type
|
19
|
-
when 'string'
|
20
|
-
Familia.redis(source_uri).get key
|
21
|
-
when 'list'
|
22
|
-
Familia.redis(source_uri).lrange key, 0, -1
|
23
|
-
when 'set'
|
24
|
-
Familia.redis(source_uri).smembers key
|
25
|
-
else
|
26
|
-
raise Familia::Problem, "unknown key type: #{type}"
|
27
|
-
end
|
28
|
-
raise 'Not implemented'
|
29
|
-
end
|
30
|
-
yield(idx, type, key, ttl) unless each_key.nil?
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Use the return value from each_key as the new key name
|
35
|
-
def rename(filter, source_uri, target_uri = nil, &each_key)
|
36
|
-
target_uri ||= source_uri
|
37
|
-
move_keys filter, source_uri, target_uri if source_uri != target_uri
|
38
|
-
source_keys = Familia.redis(source_uri).keys(filter)
|
39
|
-
puts "Renaming #{source_keys.size} keys from #{source_uri} (filter: #{filter})"
|
40
|
-
source_keys.each_with_index do |key, idx|
|
41
|
-
Familia.trace :RENAME1, Familia.redis(source_uri), "#{key}", ''
|
42
|
-
type = Familia.redis(source_uri).type key
|
43
|
-
ttl = Familia.redis(source_uri).ttl key
|
44
|
-
newkey = yield(idx, type, key, ttl) unless each_key.nil?
|
45
|
-
Familia.trace :RENAME2, Familia.redis(source_uri), "#{key} -> #{newkey}", caller(1..1).first
|
46
|
-
Familia.redis(source_uri).renamenx key, newkey
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def get_any(keyname, uri = nil)
|
51
|
-
type = Familia.redis(uri).type keyname
|
52
|
-
case type
|
53
|
-
when 'string'
|
54
|
-
Familia.redis(uri).get keyname
|
55
|
-
when 'list'
|
56
|
-
Familia.redis(uri).lrange(keyname, 0, -1) || []
|
57
|
-
when 'set'
|
58
|
-
Familia.redis(uri).smembers(keyname) || []
|
59
|
-
when 'zset'
|
60
|
-
Familia.redis(uri).zrange(keyname, 0, -1) || []
|
61
|
-
when 'hash'
|
62
|
-
Familia.redis(uri).hgetall(keyname) || {}
|
63
|
-
else
|
64
|
-
nil
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
data/lib/redis_middleware.rb
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# RedisLogger is RedisClient middleware.
|
4
|
-
#
|
5
|
-
# This middleware addresses the need for detailed Redis command logging, which
|
6
|
-
# was removed from the redis-rb gem due to performance concerns. However, in
|
7
|
-
# many development and debugging scenarios, the ability to log Redis commands
|
8
|
-
# can be invaluable.
|
9
|
-
#
|
10
|
-
# @example Enable Redis command logging
|
11
|
-
# RedisLogger.logger = Logger.new(STDOUT)
|
12
|
-
# RedisClient.register(RedisLogger)
|
13
|
-
#
|
14
|
-
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
15
|
-
#
|
16
|
-
# @note While there were concerns about the performance impact of logging in
|
17
|
-
# the redis-rb gem, this middleware is designed to be optional and can be
|
18
|
-
# easily enabled or disabled as needed. The performance impact is minimal
|
19
|
-
# when logging is disabled, and the benefits during development and debugging
|
20
|
-
# often outweigh the slight performance cost when enabled.
|
21
|
-
module RedisLogger
|
22
|
-
@logger = nil
|
23
|
-
|
24
|
-
class << self
|
25
|
-
# Gets/sets the logger instance used by RedisLogger.
|
26
|
-
# @return [Logger, nil] The current logger instance or nil if not set.
|
27
|
-
attr_accessor :logger
|
28
|
-
end
|
29
|
-
|
30
|
-
# Logs the Redis command and its execution time.
|
31
|
-
#
|
32
|
-
# This method is called for each Redis command when the middleware is active.
|
33
|
-
# It logs the command and its execution time only if a logger is set.
|
34
|
-
#
|
35
|
-
# @param command [Array] The Redis command and its arguments.
|
36
|
-
# @param redis_config [Hash] The configuration options for the Redis
|
37
|
-
# connection.
|
38
|
-
# @return [Object] The result of the Redis command execution.
|
39
|
-
#
|
40
|
-
# @note The performance impact of this logging is negligible when no logger
|
41
|
-
# is set, as it quickly returns control to the Redis client. When a logger
|
42
|
-
# is set, the minimal overhead is often offset by the valuable insights
|
43
|
-
# gained during development and debugging.
|
44
|
-
def call(command, redis_config)
|
45
|
-
return yield unless RedisLogger.logger
|
46
|
-
|
47
|
-
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
48
|
-
result = yield
|
49
|
-
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
50
|
-
RedisLogger.logger.debug("Redis: #{command.inspect} (#{duration}µs)")
|
51
|
-
result
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# RedisCommandCounter is RedisClient middleware.
|
56
|
-
#
|
57
|
-
# This middleware counts the number of Redis commands executed. It can be
|
58
|
-
# useful for performance monitoring and debugging, allowing you to track
|
59
|
-
# the volume of Redis operations in your application.
|
60
|
-
#
|
61
|
-
# @example Enable Redis command counting
|
62
|
-
# RedisCommandCounter.reset
|
63
|
-
# RedisClient.register(RedisCommandCounter)
|
64
|
-
#
|
65
|
-
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
66
|
-
module RedisCommandCounter
|
67
|
-
@count = 0
|
68
|
-
@mutex = Mutex.new
|
69
|
-
|
70
|
-
class << self
|
71
|
-
# Gets the current count of Redis commands executed.
|
72
|
-
# @return [Integer] The number of Redis commands executed.
|
73
|
-
attr_reader :count
|
74
|
-
|
75
|
-
# Resets the command count to zero.
|
76
|
-
# This method is thread-safe.
|
77
|
-
# @return [Integer] The reset count (always 0).
|
78
|
-
def reset
|
79
|
-
@mutex.synchronize { @count = 0 }
|
80
|
-
end
|
81
|
-
|
82
|
-
# Increments the command count.
|
83
|
-
# This method is thread-safe.
|
84
|
-
# @return [Integer] The new count after incrementing.
|
85
|
-
def increment
|
86
|
-
@mutex.synchronize { @count += 1 }
|
87
|
-
end
|
88
|
-
|
89
|
-
def count_commands
|
90
|
-
start_count = count
|
91
|
-
yield
|
92
|
-
end_count = count
|
93
|
-
end_count - start_count
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# Counts the Redis command and delegates its execution.
|
98
|
-
#
|
99
|
-
# This method is called for each Redis command when the middleware is active.
|
100
|
-
# It increments the command count and then yields to execute the actual command.
|
101
|
-
#
|
102
|
-
# @param command [Array] The Redis command and its arguments.
|
103
|
-
# @param redis_config [Hash] The configuration options for the Redis connection.
|
104
|
-
# @return [Object] The result of the Redis command execution.
|
105
|
-
def call(command, redis_config)
|
106
|
-
RedisCommandCounter.increment
|
107
|
-
yield
|
108
|
-
end
|
109
|
-
end
|
data/try/20_redis_type_try.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
|
2
|
-
require_relative '../lib/familia'
|
3
|
-
require_relative './test_helpers'
|
4
|
-
|
5
|
-
|
6
|
-
@limiter1 = Limiter.new :requests
|
7
|
-
|
8
|
-
|
9
|
-
## Redis Types are unique per instance of a Familia class
|
10
|
-
@a = Bone.new 'atoken1', :name1
|
11
|
-
@b = Bone.new 'atoken2', :name2
|
12
|
-
p [@a.object_id, @b.object_id]
|
13
|
-
p [@a.owners.parent.class, @b.owners.parent.class]
|
14
|
-
p [@a.owners.parent.object_id, @b.owners.parent.object_id]
|
15
|
-
p [@a.owners.rediskey, @b.owners.rediskey]
|
16
|
-
p [@a.token, @b.token]
|
17
|
-
p [@a.name, @b.name]
|
18
|
-
@a.owners.rediskey.eql?(@b.owners.rediskey)
|
19
|
-
#=> false
|
20
|
-
|
21
|
-
## Redis Types are frozen
|
22
|
-
@a.owners.frozen?
|
23
|
-
#=> true
|
24
|
-
|
25
|
-
## Limiter#qstamp
|
26
|
-
@limiter1.counter.qstamp(10.minutes, '%H:%M', 1302468980)
|
27
|
-
##=> '20:50'
|
28
|
-
|
29
|
-
## Redis Types can be stored to quantized stamp suffix
|
30
|
-
@limiter1.counter.rediskey
|
31
|
-
##=> "v1:limiter:requests:counter:20:50"
|
32
|
-
|
33
|
-
## Limiter#qstamp as a number
|
34
|
-
@limiter2 = Limiter.new :requests
|
35
|
-
p [@limiter1.ttl, @limiter2.ttl]
|
36
|
-
p [@limiter1.counter.parent.ttl, @limiter2.counter.parent.ttl]
|
37
|
-
@limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1302468980)
|
38
|
-
#=> 1302468600
|
39
|
-
|
40
|
-
## Redis Types can be stored to quantized numeric suffix. This
|
41
|
-
## tryouts is disabled b/c `RedisType#rediskey` takes no args
|
42
|
-
## and relies on the `class Limiter` definition in test_helpers.rb
|
43
|
-
## for the `:quantize` option. The quantized suffix for the Limiter
|
44
|
-
## class is `'%H:%M'` so its redis keys will always look like that.
|
45
|
-
@limiter2.counter.rediskey
|
46
|
-
##=> "v1:limiter:requests:counter:1302468600"
|
47
|
-
|
48
|
-
## Increment counter
|
49
|
-
@limiter1.counter.delete!
|
50
|
-
@limiter1.counter.increment
|
51
|
-
#=> 1
|
52
|
-
|
53
|
-
## Check counter ttl
|
54
|
-
@limiter1.counter.ttl
|
55
|
-
#=> 3600.0
|
56
|
-
|
57
|
-
## Check limiter ttl
|
58
|
-
@limiter1.ttl
|
59
|
-
#=> 1800.0
|
60
|
-
|
61
|
-
## Check ttl for a different instance
|
62
|
-
## (this exists to make sure options are cloned for each instance)
|
63
|
-
@limiter3 = Limiter.new :requests
|
64
|
-
@limiter3.counter.ttl
|
65
|
-
#=> 3600.0
|
66
|
-
|
67
|
-
## Check realttl
|
68
|
-
sleep 1 # Redis ttls are in seconds so we can't wait any less time than this (without mocking)
|
69
|
-
@limiter1.counter.realttl
|
70
|
-
#=> 3600-1
|
data/try/91_json_bug_try.rb
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
# try/91_json_bug_try.rb
|
2
|
-
|
3
|
-
require_relative '../lib/familia'
|
4
|
-
require_relative './test_helpers'
|
5
|
-
|
6
|
-
Familia.debug = false
|
7
|
-
|
8
|
-
# Define a simple model with fields that should handle JSON data
|
9
|
-
class JsonTest < Familia::Horreum
|
10
|
-
identifier :id
|
11
|
-
field :id
|
12
|
-
field :config # This should be able to store Hash objects
|
13
|
-
field :tags # This should be able to store Array objects
|
14
|
-
field :simple # This should store simple strings as-is
|
15
|
-
end
|
16
|
-
|
17
|
-
# Create an instance with JSON data
|
18
|
-
@test_obj = JsonTest.new
|
19
|
-
@test_obj.id = "json_test_1"
|
20
|
-
|
21
|
-
## Test 1: Store a Hash - should serialize to JSON automatically
|
22
|
-
@test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
|
23
|
-
@test_obj.config.class
|
24
|
-
#=> Hash
|
25
|
-
|
26
|
-
## Test 2: Store an Array - should serialize to JSON automatically
|
27
|
-
@test_obj.tags = ["ruby", "redis", "json", "familia"]
|
28
|
-
@test_obj.tags.class
|
29
|
-
#=> Array
|
30
|
-
|
31
|
-
## Test 3: Store a simple string - should remain as string
|
32
|
-
@test_obj.simple = "just a string"
|
33
|
-
@test_obj.simple.class
|
34
|
-
#=> String
|
35
|
-
|
36
|
-
## Save the object - this should call serialize_value and use to_json
|
37
|
-
@test_obj.save
|
38
|
-
#=> true
|
39
|
-
|
40
|
-
## Verify what's actually stored in Redis (raw)
|
41
|
-
raw_data = @test_obj.hgetall
|
42
|
-
p [:plop, @test_obj]
|
43
|
-
puts "Raw Redis data:"
|
44
|
-
raw_data
|
45
|
-
#=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"redis\",\"json\",\"familia\"]", "simple"=>"just a string", "key"=>"json_test_1"}
|
46
|
-
|
47
|
-
## BUG: After refresh, JSON data comes back as strings instead of parsed objects
|
48
|
-
@test_obj.refresh!
|
49
|
-
|
50
|
-
## Test 4: Hash should be deserialized back to Hash
|
51
|
-
puts "Config after refresh:"
|
52
|
-
puts @test_obj.config.inspect
|
53
|
-
puts "Config class: "
|
54
|
-
[@test_obj.config.class, @test_obj.config.inspect]
|
55
|
-
#=> [Hash, "{:theme=>\"dark\", :notifications=>true, :settings=>{:volume=>80}}"]
|
56
|
-
|
57
|
-
## Test 5: Array should be deserialized back to Array
|
58
|
-
puts "Tags after refresh:"
|
59
|
-
puts @test_obj.tags.inspect
|
60
|
-
puts "Tags class: #{@test_obj.tags.class}"
|
61
|
-
@test_obj.tags.inspect
|
62
|
-
@test_obj.tags.class
|
63
|
-
#=> ["ruby", "redis", "json", "familia"]
|
64
|
-
#=> Array
|
65
|
-
|
66
|
-
## Test 6: Simple string should remain a string (this works correctly)
|
67
|
-
puts "Simple after refresh:"
|
68
|
-
puts @test_obj.simple.inspect
|
69
|
-
puts "Simple class: #{@test_obj.simple.class}"
|
70
|
-
@test_obj.simple.inspect
|
71
|
-
@test_obj.simple.class
|
72
|
-
#=> "just a string"
|
73
|
-
#=> String
|
74
|
-
|
75
|
-
## Demonstrate the asymmetry:
|
76
|
-
puts "\n=== ASYMMETRY DEMONSTRATION ==="
|
77
|
-
puts "Before save: config is #{@test_obj.config.class}"
|
78
|
-
@test_obj.config = { example: "data" }
|
79
|
-
puts "After assignment: config is #{@test_obj.config.class}"
|
80
|
-
@test_obj.save
|
81
|
-
puts "After save: config is still #{@test_obj.config.class}"
|
82
|
-
@test_obj.refresh!
|
83
|
-
puts "After refresh: config is now #{@test_obj.config.class}!"
|
84
|
-
|
85
|
-
## Clean up
|
86
|
-
@test_obj.destroy!
|