familia 2.0.0.pre.pre → 2.0.0.pre3
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/CLAUDE.md +12 -5
- data/Gemfile +4 -3
- data/Gemfile.lock +24 -11
- data/bin/irb +1 -1
- data/docs/connection_pooling.md +98 -223
- data/familia.gemspec +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/core_ext.rb +2 -2
- data/lib/familia/features/expiration.rb +0 -1
- data/lib/familia/features/relatable_objects.rb +127 -0
- data/lib/familia/features.rb +7 -3
- data/lib/familia/horreum/class_methods.rb +18 -4
- data/lib/familia/secure_identifier.rb +129 -0
- data/lib/familia/utils.rb +7 -96
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -1
- data/try/configuration/scenarios_try.rb +43 -31
- data/try/core/connection_try.rb +1 -1
- data/try/core/errors_try.rb +10 -10
- data/try/core/extensions_try.rb +56 -23
- data/try/core/familia_extended_try.rb +3 -3
- data/try/core/familia_try.rb +2 -6
- data/try/core/middleware_try.rb +34 -40
- data/try/{pooling/connection_pool_test_try.rb → core/pools_try.rb} +2 -2
- data/try/core/secure_identifier_try.rb +104 -0
- data/try/core/tools_try.rb +52 -36
- data/try/core/utils_try.rb +0 -98
- data/try/datatypes/boolean_try.rb +6 -7
- data/try/datatypes/datatype_base_try.rb +2 -2
- data/try/datatypes/hash_try.rb +0 -1
- data/try/datatypes/list_try.rb +0 -1
- data/try/datatypes/set_try.rb +0 -2
- data/try/datatypes/sorted_set_try.rb +1 -2
- data/try/datatypes/string_try.rb +1 -2
- data/try/edge_cases/empty_identifiers_try.rb +42 -35
- data/try/edge_cases/hash_symbolization_try.rb +5 -5
- data/try/edge_cases/json_serialization_try.rb +12 -13
- data/try/edge_cases/race_conditions_try.rb +46 -49
- data/try/edge_cases/reserved_keywords_try.rb +103 -49
- data/try/edge_cases/string_coercion_try.rb +2 -2
- data/try/edge_cases/ttl_side_effects_try.rb +44 -25
- data/try/features/expiration_try.rb +2 -2
- data/try/features/quantization_try.rb +2 -2
- data/try/features/relatable_objects_try.rb +221 -0
- data/try/features/safe_dump_advanced_try.rb +13 -14
- data/try/features/safe_dump_try.rb +8 -8
- data/try/helpers/test_helpers.rb +10 -12
- data/try/horreum/base_try.rb +9 -9
- data/try/horreum/class_methods_try.rb +34 -28
- data/try/horreum/commands_try.rb +69 -33
- data/try/horreum/initialization_try.rb +4 -4
- data/try/horreum/relations_try.rb +13 -14
- data/try/horreum/serialization_try.rb +3 -3
- data/try/horreum/settings_try.rb +25 -31
- data/try/integration/cross_component_try.rb +45 -35
- data/try/models/customer_safe_dump_try.rb +4 -4
- data/try/models/customer_try.rb +22 -25
- data/try/models/datatype_base_try.rb +2 -4
- data/try/models/familia_object_try.rb +3 -4
- data/try/performance/benchmarks_try.rb +47 -38
- data/try/prototypes/atomic_saves_v4.rb +3 -3
- metadata +18 -15
- data/try/core/refinements_try.rb +0 -39
- /data/try/{pooling → prototypes/pooling}/README.md +0 -0
- /data/try/{pooling/configurable_stress_test_try.rb → prototypes/pooling/configurable_stress_test.rb} +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_metrics.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/visualize_stress_results.rb +0 -0
- /data/try/{pooling/pool_siege_try.rb → prototypes/pooling/pool_siege.rb} +0 -0
- /data/try/{pooling/run_stress_tests_try.rb → prototypes/pooling/run_stress_tests.rb} +0 -0
@@ -22,7 +22,7 @@ p [@a.name, @b.name]
|
|
22
22
|
#=> true
|
23
23
|
|
24
24
|
## Limiter#qstamp
|
25
|
-
@limiter1.counter.qstamp(10.minutes, '%H:%M',
|
25
|
+
@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)
|
26
26
|
##=> '20:50'
|
27
27
|
|
28
28
|
## Database Types can be stored to quantized stamp suffix
|
@@ -33,7 +33,7 @@ p [@a.name, @b.name]
|
|
33
33
|
@limiter2 = Limiter.new :requests
|
34
34
|
p [@limiter1.default_expiration, @limiter2.default_expiration]
|
35
35
|
p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default_expiration]
|
36
|
-
@limiter2.counter.qstamp(10.minutes, pattern: nil, time:
|
36
|
+
@limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1_302_468_980)
|
37
37
|
#=> 1302468600
|
38
38
|
|
39
39
|
## Database Types can be stored to quantized numeric suffix. This
|
data/try/datatypes/hash_try.rb
CHANGED
data/try/datatypes/list_try.rb
CHANGED
data/try/datatypes/set_try.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative '../../lib/familia'
|
4
4
|
require_relative '../helpers/test_helpers'
|
5
5
|
|
6
|
-
|
7
6
|
@a = Bone.new 'atoken'
|
8
7
|
|
9
8
|
## Familia::SortedSet#add
|
@@ -36,7 +35,7 @@ require_relative '../helpers/test_helpers'
|
|
36
35
|
#=> ['metric1', 'metric2', 'metric3']
|
37
36
|
|
38
37
|
## Familia::SortedSet#rangebyscore with a limit
|
39
|
-
@a.metrics.rangebyscore 1, 3, :
|
38
|
+
@a.metrics.rangebyscore 1, 3, limit: [0, 2]
|
40
39
|
#=> ['metric1', 'metric2']
|
41
40
|
|
42
41
|
## Familia::SortedSet#increment
|
data/try/datatypes/string_try.rb
CHANGED
@@ -14,7 +14,7 @@ require_relative '../helpers/test_helpers'
|
|
14
14
|
#=> 'GREAT!'
|
15
15
|
|
16
16
|
## Familia::String#value=
|
17
|
-
@a.value.value =
|
17
|
+
@a.value.value = 'DECENT!'
|
18
18
|
#=> 'DECENT!'
|
19
19
|
|
20
20
|
## Familia::String#to_s
|
@@ -62,5 +62,4 @@ require_relative '../helpers/test_helpers'
|
|
62
62
|
@ret.value
|
63
63
|
#=> '1050bytes'
|
64
64
|
|
65
|
-
|
66
65
|
@ret.delete!
|
@@ -1,48 +1,55 @@
|
|
1
|
-
|
1
|
+
# try/edge_cases/empty_identifiers_try.rb
|
2
2
|
|
3
3
|
# Test empty identifier edge cases
|
4
|
-
group "Empty Identifier Edge Cases"
|
5
4
|
|
6
|
-
|
7
|
-
@user_class = Class.new(Familia::Horreum) do
|
8
|
-
identifier :email
|
9
|
-
field :name
|
10
|
-
end
|
11
|
-
end
|
5
|
+
require_relative '../helpers/test_helpers'
|
12
6
|
|
13
|
-
try "empty string identifier causes stack overflow" do
|
14
|
-
user = @user_class.new(email: "", name: "Test")
|
15
7
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
false # Unexpected error
|
8
|
+
## empty string identifier handling
|
9
|
+
begin
|
10
|
+
user_class = Class.new(Familia::Horreum) do
|
11
|
+
identifier_field :email
|
12
|
+
field :email
|
13
|
+
field :name
|
23
14
|
end
|
15
|
+
user = user_class.new(email: '', name: 'Test')
|
16
|
+
user.exists? # Test actual behavior with empty identifier
|
17
|
+
rescue SystemStackError
|
18
|
+
'stack_overflow' # Stack overflow occurred
|
19
|
+
rescue StandardError => e
|
20
|
+
e.class.name # Other error occurred
|
24
21
|
end
|
22
|
+
#=> "Familia::NoIdentifier"
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
rescue SystemStackError, Familia::NoIdentifier
|
33
|
-
true # Expected error
|
24
|
+
## nil identifier handling
|
25
|
+
begin
|
26
|
+
user_class = Class.new(Familia::Horreum) do
|
27
|
+
identifier_field :email
|
28
|
+
field :email
|
29
|
+
field :name
|
34
30
|
end
|
31
|
+
user = user_class.new(email: nil, name: 'Test')
|
32
|
+
user.exists?
|
33
|
+
rescue SystemStackError
|
34
|
+
'stack_overflow'
|
35
|
+
rescue Familia::NoIdentifier => e
|
36
|
+
'no_identifier'
|
37
|
+
rescue StandardError => e
|
38
|
+
e.class.name
|
35
39
|
end
|
40
|
+
#=> "no_identifier"
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
## empty identifier validation check
|
43
|
+
begin
|
44
|
+
user_class = Class.new(Familia::Horreum) do
|
45
|
+
identifier_field :email
|
46
|
+
field :email
|
47
|
+
field :name
|
43
48
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
user = user_class.new(email: '', name: 'Test')
|
50
|
+
# Check if identifier is empty
|
51
|
+
user.identifier.to_s.empty?
|
52
|
+
rescue StandardError => e
|
53
|
+
e.class.name
|
48
54
|
end
|
55
|
+
#=> true
|
@@ -16,9 +16,9 @@ class SymbolizeTest < Familia::Horreum
|
|
16
16
|
field :config
|
17
17
|
end
|
18
18
|
|
19
|
-
@test_hash = {
|
19
|
+
@test_hash = { 'name' => 'John', 'age' => 30, 'nested' => { 'theme' => 'dark' } }
|
20
20
|
@test_obj = SymbolizeTest.new
|
21
|
-
@test_obj.id =
|
21
|
+
@test_obj.id = 'symbolize_test_1'
|
22
22
|
@test_obj.config = @test_hash
|
23
23
|
@test_obj.save
|
24
24
|
|
@@ -55,18 +55,18 @@ end
|
|
55
55
|
#=> ["name", "age", "nested"]
|
56
56
|
|
57
57
|
## Nested hash in string result also has string keys
|
58
|
-
@string_result[
|
58
|
+
@string_result['nested'].keys
|
59
59
|
#=> ["theme"]
|
60
60
|
|
61
61
|
## Values are preserved correctly in both cases
|
62
62
|
@symbol_result[:name]
|
63
63
|
#=> "John"
|
64
64
|
|
65
|
-
@string_result[
|
65
|
+
@string_result['name']
|
66
66
|
#=> "John"
|
67
67
|
|
68
68
|
## Arrays are handled correctly too
|
69
|
-
@test_obj.config = [{
|
69
|
+
@test_obj.config = [{ 'item' => 'value' }, 'string', 123]
|
70
70
|
@test_obj.save
|
71
71
|
@array_json = @test_obj.hget('config')
|
72
72
|
#=> "[{\"item\":\"value\"},\"string\",123]"
|
@@ -14,22 +14,21 @@ class JsonTest < Familia::Horreum
|
|
14
14
|
field :simple # This should store simple strings as-is
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
17
|
## Test 1: Store a Hash - should serialize to JSON automatically
|
19
18
|
test_obj = JsonTest.new
|
20
|
-
test_obj.config = { theme:
|
19
|
+
test_obj.config = { theme: 'dark', notifications: true, settings: { volume: 80 } }
|
21
20
|
test_obj.config
|
22
21
|
#=:> Hash
|
23
22
|
|
24
23
|
## Test 2: Store an Array - should serialize to JSON automatically
|
25
24
|
test_obj = JsonTest.new
|
26
|
-
test_obj.tags = [
|
25
|
+
test_obj.tags = %w[ruby valkey json familia]
|
27
26
|
test_obj.tags
|
28
27
|
#=:> Array
|
29
28
|
|
30
29
|
## Test 3: Store a simple string - should remain as string
|
31
30
|
test_obj = JsonTest.new
|
32
|
-
test_obj.simple =
|
31
|
+
test_obj.simple = 'just a string'
|
33
32
|
test_obj.simple
|
34
33
|
#=:> String
|
35
34
|
|
@@ -40,25 +39,25 @@ test_obj.save
|
|
40
39
|
|
41
40
|
## Verify what's actually stored in Database (raw)
|
42
41
|
test_obj = JsonTest.new
|
43
|
-
test_obj.id =
|
44
|
-
test_obj.config = { theme:
|
45
|
-
test_obj.simple =
|
46
|
-
test_obj.tags = [
|
42
|
+
test_obj.id = 'json_test_1'
|
43
|
+
test_obj.config = { theme: 'dark', notifications: true, settings: { volume: 80 } }
|
44
|
+
test_obj.simple = 'just a string'
|
45
|
+
test_obj.tags = %w[ruby valkey json familia]
|
47
46
|
test_obj.save
|
48
47
|
test_obj.hgetall
|
49
48
|
#=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
|
50
49
|
|
51
50
|
## Test 4: Hash should be deserialized back to Hash
|
52
51
|
test_obj = JsonTest.new 'any_id_will_do'
|
53
|
-
puts
|
52
|
+
puts 'Config after refresh:'
|
54
53
|
puts test_obj.config
|
55
|
-
puts
|
54
|
+
puts 'Config class: '
|
56
55
|
[test_obj.config.class, test_obj.config]
|
57
56
|
##=> [Hash, {:theme=>"dark", :notifications=>true, :settings=>{:volume=>80}}]
|
58
57
|
|
59
58
|
## Test 5: Array should be deserialized back to Array
|
60
59
|
test_obj = JsonTest.new 'any_id_will_do'
|
61
|
-
puts
|
60
|
+
puts 'Tags after refresh:'
|
62
61
|
puts test_obj.tags.inspect
|
63
62
|
puts "Tags class: #{test_obj.tags.class}"
|
64
63
|
test_obj.tags.inspect
|
@@ -67,7 +66,7 @@ test_obj.tags
|
|
67
66
|
|
68
67
|
## Test 6: Simple string should remain a string (this works correctly)
|
69
68
|
test_obj = JsonTest.new 'any_id_will_do'
|
70
|
-
puts
|
69
|
+
puts 'Simple after refresh:'
|
71
70
|
puts test_obj.simple.inspect
|
72
71
|
puts "Simple class: #{test_obj.simple.class}"
|
73
72
|
[test_obj.simple.class, test_obj.simple]
|
@@ -77,7 +76,7 @@ puts "Simple class: #{test_obj.simple.class}"
|
|
77
76
|
test_obj = JsonTest.new 'any_id_will_do'
|
78
77
|
puts "\n=== ASYMMETRY DEMONSTRATION ==="
|
79
78
|
puts "Before save: config is #{test_obj.config.class}"
|
80
|
-
test_obj.config = { example:
|
79
|
+
test_obj.config = { example: 'data' }
|
81
80
|
puts "After assignment: config is #{test_obj.config.class}"
|
82
81
|
test_obj.save
|
83
82
|
puts "After save: config is still #{test_obj.config.class}"
|
@@ -1,60 +1,57 @@
|
|
1
|
-
require_relative '../helpers/test_helpers'
|
2
|
-
|
3
1
|
# Test connection race conditions
|
4
|
-
group "Race Conditions Edge Cases"
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
## concurrent connection access test
|
6
|
+
user_class = Class.new(Familia::Horreum) do
|
7
|
+
identifier_field :email
|
8
|
+
field :email
|
9
|
+
field :counter
|
11
10
|
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
user.save
|
16
|
-
|
17
|
-
threads = []
|
18
|
-
results = []
|
19
|
-
|
20
|
-
# Simulate high concurrency
|
21
|
-
10.times do
|
22
|
-
threads << Thread.new do
|
23
|
-
begin
|
24
|
-
user.incr(:counter)
|
25
|
-
results << "success"
|
26
|
-
rescue => e
|
27
|
-
results << "error: #{e.class.name}"
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
12
|
+
user = user_class.new(email: 'test@example.com', counter: 0)
|
13
|
+
user.save
|
31
14
|
|
32
|
-
|
15
|
+
threads = []
|
16
|
+
results = []
|
33
17
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
18
|
+
# Simulate high concurrency
|
19
|
+
10.times do
|
20
|
+
threads << Thread.new do
|
21
|
+
user.incr(:counter)
|
22
|
+
results << 'success'
|
23
|
+
rescue StandardError => e
|
24
|
+
results << "error: #{e.class.name}"
|
25
|
+
end
|
39
26
|
end
|
40
27
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
28
|
+
threads.each(&:join)
|
29
|
+
user.delete!
|
30
|
+
|
31
|
+
# Count successful operations
|
32
|
+
successes = results.count { |r| r == 'success' }
|
33
|
+
successes > 0 # Should have some successes
|
34
|
+
#=!> StandardError
|
35
|
+
|
36
|
+
## connection pool stress test
|
37
|
+
## We're just checking whether it completes within a reasonable time frame.
|
38
|
+
## If it does fail either bc of the duration or contention then it's a problem.
|
39
|
+
success_count = 0
|
40
|
+
threads = []
|
41
|
+
mutex = Mutex.new
|
42
|
+
|
43
|
+
# Test concurrent connections
|
44
|
+
100.times do |i|
|
45
|
+
threads << Thread.new do
|
46
|
+
# Try to get a connection and perform an operation
|
47
|
+
dbclient = Familia.dbclient
|
48
|
+
dbclient.ping
|
49
|
+
success_count += 1
|
52
50
|
end
|
51
|
+
end
|
53
52
|
|
54
|
-
|
53
|
+
threads.each(&:join)
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
users.each(&:delete!) rescue nil
|
60
|
-
end
|
55
|
+
# Should have some successful connections
|
56
|
+
success_count > 0
|
57
|
+
#=%> 220
|
@@ -1,59 +1,113 @@
|
|
1
|
+
# Test reserved keyword handling
|
2
|
+
|
1
3
|
require_relative '../helpers/test_helpers'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
identifier :email
|
9
|
-
# These should fail with reserved keywords
|
10
|
-
begin
|
11
|
-
field :ttl # Reserved for expiration
|
12
|
-
field :db # Reserved for database
|
13
|
-
field :redis # Reserved for connection
|
14
|
-
rescue => e
|
15
|
-
# Expected to fail
|
16
|
-
end
|
17
|
-
|
18
|
-
# Workarounds
|
19
|
-
field :secret_ttl
|
20
|
-
field :user_db
|
21
|
-
field :redis_config
|
22
|
-
end
|
5
|
+
## attempting to use ttl as field name causes error
|
6
|
+
TestClass = Class.new(Familia::Horreum) do
|
7
|
+
identifier_field :email
|
8
|
+
field :email
|
9
|
+
field :default_expiration
|
23
10
|
end
|
24
11
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
12
|
+
user = TestClass.new(email: 'test@example.com', ttl: 3600)
|
13
|
+
user.save
|
14
|
+
result = user.ttl == 3600
|
15
|
+
user.delete!
|
16
|
+
result
|
17
|
+
#=!> StandardError
|
18
|
+
|
19
|
+
## prefixed field names work as expected
|
20
|
+
TestClass2 = Class.new(Familia::Horreum) do
|
21
|
+
identifier_field :email
|
22
|
+
field :email
|
23
|
+
field :secret_ttl
|
24
|
+
field :user_db
|
25
|
+
field :dbclient_config
|
34
26
|
end
|
35
27
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
28
|
+
user = TestClass2.new(email: 'test@example.com')
|
29
|
+
user.secret_ttl = 3600
|
30
|
+
user.user_db = 5
|
31
|
+
user.dbclient_config = { host: 'localhost' }
|
32
|
+
user.save
|
33
|
+
|
34
|
+
result = user.secret_ttl == 3600 &&
|
35
|
+
user.user_db == 5 &&
|
36
|
+
user.dbclient_config.is_a?(Hash)
|
37
|
+
|
38
|
+
user.delete!
|
39
|
+
result
|
40
|
+
#=> true
|
41
|
+
|
42
|
+
## reserved methods still work normally
|
43
|
+
TestClass3 = Class.new(Familia::Horreum) do
|
44
|
+
# Note: Does not enable expiration feature
|
45
|
+
identifier_field :email
|
46
|
+
field :email
|
48
47
|
end
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
user = TestClass3.new(email: 'test@example.com')
|
50
|
+
user.save
|
51
|
+
user.delete!
|
52
|
+
user
|
53
|
+
# These should be available as methods even though we can't use them as field names
|
54
|
+
#=/=> _.respond_to?(:default_expiration)
|
55
|
+
#==> _.respond_to?(:logical_database)
|
56
|
+
#==> _.respond_to?(:dbclient)
|
57
|
+
|
53
58
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
+
## Attempting to set default_expiration on an instance with expiration feature enabled
|
60
|
+
TestClass4 = Class.new(Familia::Horreum) do
|
61
|
+
feature :expiration
|
62
|
+
identifier_field :email
|
63
|
+
field :email
|
64
|
+
field :default_expiration
|
59
65
|
end
|
66
|
+
|
67
|
+
user = TestClass4.new(email: 'test@example.com', default_expiration: 3600)
|
68
|
+
user.save
|
69
|
+
user.delete!
|
70
|
+
user
|
71
|
+
#==> _.default_expiration == 3600
|
72
|
+
|
73
|
+
## prefixed field names work as expected
|
74
|
+
TestClass5 = Class.new(Familia::Horreum) do
|
75
|
+
identifier_field :email
|
76
|
+
field :email
|
77
|
+
field :secret_ttl
|
78
|
+
field :user_db
|
79
|
+
field :dbclient_config
|
80
|
+
end
|
81
|
+
|
82
|
+
user = TestClass5.new(email: 'test@example.com')
|
83
|
+
user.secret_ttl = 3600
|
84
|
+
user.user_db = 5
|
85
|
+
user.dbclient_config = { host: 'localhost' }
|
86
|
+
user.save
|
87
|
+
|
88
|
+
result = user.secret_ttl == 3600 &&
|
89
|
+
user.user_db == 5 &&
|
90
|
+
user.dbclient_config.is_a?(Hash)
|
91
|
+
|
92
|
+
user.delete!
|
93
|
+
result
|
94
|
+
#=> true
|
95
|
+
|
96
|
+
## reserved methods still work normally
|
97
|
+
TestClass6 = Class.new(Familia::Horreum) do
|
98
|
+
feature :expiration
|
99
|
+
identifier_field :email
|
100
|
+
field :email
|
101
|
+
end
|
102
|
+
|
103
|
+
user = TestClass6.new(email: 'test@example.com')
|
104
|
+
user.save
|
105
|
+
|
106
|
+
# These should be available as methods even though we can't use them as field names
|
107
|
+
result = user.respond_to?(:default_expiration) &&
|
108
|
+
user.respond_to?(:logical_database) &&
|
109
|
+
user.respond_to?(:dbclient)
|
110
|
+
|
111
|
+
user.delete!
|
112
|
+
result
|
113
|
+
#=> true
|
@@ -35,7 +35,7 @@ def lookup_by_id(id_string)
|
|
35
35
|
end
|
36
36
|
|
37
37
|
## Instantiaite a troubled model class
|
38
|
-
@bad_obj =
|
38
|
+
@bad_obj = BadIdentifierTest.new
|
39
39
|
#=:> BadIdentifierTest
|
40
40
|
|
41
41
|
# Test polymorphic string usage for Familia objects
|
@@ -140,7 +140,7 @@ process_identifier(@customer)
|
|
140
140
|
|
141
141
|
## to_s handles identifier errors gracefully
|
142
142
|
badboi = BadIdentifierTest.new
|
143
|
-
badboi.to_s
|
143
|
+
badboi.to_s # .include?('BadIdentifierTest')
|
144
144
|
#=~> /BadIdentifierTest:0x[0-9a-f]+/
|
145
145
|
|
146
146
|
## Performance consideration: to_s caching behavior
|
@@ -1,51 +1,70 @@
|
|
1
|
-
require_relative '../helpers/test_helpers'
|
2
|
-
|
3
1
|
# Test TTL side effects
|
4
|
-
group "TTL Side Effects Edge Cases"
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
## field update behavior with TTL
|
6
|
+
begin
|
7
|
+
session_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier_field :session_id
|
9
|
+
field :session_id
|
9
10
|
field :name
|
10
11
|
field :data
|
11
12
|
feature :expiration
|
12
|
-
|
13
|
+
default_expiration 300 # 5 minutes
|
13
14
|
end
|
14
|
-
end
|
15
15
|
|
16
|
-
|
17
|
-
session = @session_class.new(session_id: "test123", name: "Session")
|
16
|
+
session = session_class.new(session_id: 'test123', name: 'Session')
|
18
17
|
session.save
|
19
18
|
|
20
19
|
# Set shorter TTL
|
21
20
|
session.expire(60)
|
22
21
|
original_ttl = session.realttl
|
23
22
|
|
24
|
-
# Update field
|
25
|
-
session.name =
|
23
|
+
# Update field
|
24
|
+
session.name = 'Updated Session'
|
26
25
|
session.save
|
27
26
|
|
28
27
|
new_ttl = session.realttl
|
29
28
|
|
30
|
-
#
|
31
|
-
new_ttl > original_ttl
|
32
|
-
|
33
|
-
|
29
|
+
# Check if TTL was preserved or reset
|
30
|
+
result = new_ttl > original_ttl # true if TTL was reset (side effect)
|
31
|
+
session.delete!
|
32
|
+
result
|
33
|
+
rescue StandardError => e
|
34
|
+
session&.delete! rescue nil
|
35
|
+
false
|
34
36
|
end
|
37
|
+
#=> false
|
38
|
+
|
39
|
+
## batch update attempts to preserve TTL
|
40
|
+
begin
|
41
|
+
session_class = Class.new(Familia::Horreum) do
|
42
|
+
identifier_field :session_id
|
43
|
+
field :session_id
|
44
|
+
field :name
|
45
|
+
feature :expiration
|
46
|
+
default_expiration 300
|
47
|
+
end
|
35
48
|
|
36
|
-
|
37
|
-
session = @session_class.new(session_id: "test124")
|
49
|
+
session = session_class.new(session_id: 'test124')
|
38
50
|
session.save
|
39
51
|
session.expire(60)
|
40
52
|
|
41
53
|
original_ttl = session.realttl
|
42
54
|
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
55
|
+
# Try batch update (if available)
|
56
|
+
begin
|
57
|
+
session.batch_update({ name: 'Batch Updated' }, update_expiration: false)
|
58
|
+
new_ttl = session.realttl
|
59
|
+
result = (original_ttl - new_ttl).abs < 5 # TTL preserved within tolerance
|
60
|
+
rescue NoMethodError
|
61
|
+
result = true # Method not available, assume test passes
|
62
|
+
end
|
47
63
|
|
48
|
-
|
49
|
-
|
50
|
-
|
64
|
+
session.delete!
|
65
|
+
result
|
66
|
+
rescue StandardError => e
|
67
|
+
session&.delete! rescue nil
|
68
|
+
true
|
51
69
|
end
|
70
|
+
#=> true
|