familia 1.2.0 → 2.0.0.pre.pre
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 +15 -2
- data/Gemfile.lock +76 -34
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +317 -0
- data/familia.gemspec +9 -5
- 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 +17 -12
- 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} +5 -3
- 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} +20 -17
- data/try/models/datatype_base_try.rb +101 -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 +140 -43
- 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
@@ -0,0 +1,86 @@
|
|
1
|
+
# try/features/expiration_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
# Test expiration feature functionality
|
9
|
+
|
10
|
+
class ParentExpiring < Familia::Horreum
|
11
|
+
feature :expiration
|
12
|
+
default_expiration 1000
|
13
|
+
end
|
14
|
+
|
15
|
+
class ChildExpiring < ParentExpiring
|
16
|
+
identifier_field :id
|
17
|
+
field :id
|
18
|
+
default_expiration 1000
|
19
|
+
end
|
20
|
+
|
21
|
+
# Define a test class with expiration feature
|
22
|
+
class ExpiringTest < Familia::Horreum
|
23
|
+
feature :expiration
|
24
|
+
identifier_field :id
|
25
|
+
field :id
|
26
|
+
field :data
|
27
|
+
default_expiration 300 # 5 minutes
|
28
|
+
end
|
29
|
+
|
30
|
+
# Setup test object
|
31
|
+
@test_obj = ExpiringTest.new
|
32
|
+
@test_obj.id = "expire_test_1"
|
33
|
+
@test_obj.data = "test data"
|
34
|
+
|
35
|
+
## Class has default_expiration method from feature
|
36
|
+
ExpiringTest.respond_to?(:default_expiration)
|
37
|
+
#=> true
|
38
|
+
|
39
|
+
## Class has default expiration set
|
40
|
+
ExpiringTest.default_expiration
|
41
|
+
#=> 300.0
|
42
|
+
|
43
|
+
## Can set different default expiration on class
|
44
|
+
ExpiringTest.default_expiration(600)
|
45
|
+
ExpiringTest.default_expiration
|
46
|
+
#=> 600.0
|
47
|
+
|
48
|
+
## Object inherits class default expiration
|
49
|
+
@test_obj.default_expiration
|
50
|
+
#=> 600.0
|
51
|
+
|
52
|
+
## Can set default expiration on individual object
|
53
|
+
@test_obj.default_expiration = 120
|
54
|
+
@test_obj.default_expiration
|
55
|
+
#=> 120.0
|
56
|
+
|
57
|
+
## Object has update_expiration method
|
58
|
+
@test_obj.respond_to?(:update_expiration)
|
59
|
+
#=> true
|
60
|
+
|
61
|
+
## Can call update_expiration method
|
62
|
+
result = @test_obj.update_expiration(default_expiration: 180)
|
63
|
+
[result.class, result]
|
64
|
+
#=> [FalseClass, false]
|
65
|
+
|
66
|
+
## Child inherits parent default expiration when not set
|
67
|
+
ChildExpiring.default_expiration
|
68
|
+
#=> 1000.0
|
69
|
+
|
70
|
+
## Can override parent default expiration
|
71
|
+
ChildExpiring.default_expiration(500)
|
72
|
+
ChildExpiring.default_expiration
|
73
|
+
#=> 500.0
|
74
|
+
|
75
|
+
## Falls back to Familia.default_expiration when no class/parent default expiration
|
76
|
+
class NoDefaultExpirationTest < Familia::Horreum
|
77
|
+
feature :expiration
|
78
|
+
identifier_field :id
|
79
|
+
field :id
|
80
|
+
end
|
81
|
+
NoDefaultExpirationTest.default_expiration
|
82
|
+
#=> 0.0
|
83
|
+
|
84
|
+
# Cleanup
|
85
|
+
@test_obj.destroy!
|
86
|
+
ExpiringTest.default_expiration(300) # Reset to original
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# try/features/quantization_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
# Test quantization feature functionality
|
9
|
+
|
10
|
+
# Define a test class with quantization feature
|
11
|
+
class QuantizedTest < Familia::Horreum
|
12
|
+
feature :quantization
|
13
|
+
identifier_field :id
|
14
|
+
field :id
|
15
|
+
field :data
|
16
|
+
default_expiration 300 # 5 minutes for testing
|
17
|
+
end
|
18
|
+
|
19
|
+
## Class has qstamp method from feature
|
20
|
+
QuantizedTest.respond_to?(:qstamp)
|
21
|
+
#=> true
|
22
|
+
|
23
|
+
## Object has qstamp method from feature
|
24
|
+
@test_obj = QuantizedTest.new
|
25
|
+
@test_obj.respond_to?(:qstamp)
|
26
|
+
#=> true
|
27
|
+
|
28
|
+
## Familia has global qstamp method
|
29
|
+
Familia.respond_to?(:qstamp)
|
30
|
+
#=> true
|
31
|
+
|
32
|
+
## Familia has now method for current time
|
33
|
+
Familia.respond_to?(:now)
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## Can get current time with Familia.now
|
37
|
+
now = Familia.now
|
38
|
+
now.class
|
39
|
+
#=> Float
|
40
|
+
|
41
|
+
## qstamp with no arguments returns current quantized timestamp
|
42
|
+
stamp1 = QuantizedTest.qstamp
|
43
|
+
p [:QuantizedTest, QuantizedTest.qstamp]
|
44
|
+
stamp1.class
|
45
|
+
#=> Integer
|
46
|
+
|
47
|
+
## qstamp with quantum returns quantized timestamp
|
48
|
+
stamp2 = QuantizedTest.qstamp(3600) # 1 hour quantum
|
49
|
+
stamp2.class
|
50
|
+
#=> Integer
|
51
|
+
|
52
|
+
## qstamp with quantum and pattern returns formatted string
|
53
|
+
stamp3 = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H')
|
54
|
+
stamp3.class
|
55
|
+
#=> String
|
56
|
+
|
57
|
+
## qstamp with time argument
|
58
|
+
stamp4 = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H', time: Time.new(2023, 6, 15, 14, 30, 0))
|
59
|
+
stamp4.class
|
60
|
+
#=> String
|
61
|
+
|
62
|
+
## Object qstamp works same as class method
|
63
|
+
obj_stamp = @test_obj.qstamp(3600)
|
64
|
+
obj_stamp.class
|
65
|
+
#=> Integer
|
66
|
+
|
67
|
+
## Different quantum values produce different buckets
|
68
|
+
test_time = Time.utc(2023, 6, 15, 14, 30, 0) # use a fixed time, mid-day (avoid ToD boundary)
|
69
|
+
@hour_stamp = QuantizedTest.qstamp(3600, time: test_time)
|
70
|
+
@day_stamp = QuantizedTest.qstamp(86400, time: test_time)
|
71
|
+
#=> 1686787200
|
72
|
+
#=<> @hour_stamp
|
73
|
+
#==> @hour_stamp == 1686837600
|
74
|
+
|
75
|
+
## Pattern formatting works correctly
|
76
|
+
time_str = QuantizedTest.qstamp(3600, pattern: '%Y-%m-%d %H:00:00')
|
77
|
+
time_str.match?(/\d{4}-\d{2}-\d{2} \d{2}:00:00/)
|
78
|
+
#=> true
|
79
|
+
|
80
|
+
## Can pass custom time to qstamp
|
81
|
+
test_time = Time.utc(2023, 6, 15, 14, 30, 1)
|
82
|
+
# NOTE: _Not_ Time.new(2023, 6, 15, 14, 30, 1).utc which is the current time
|
83
|
+
# locally where this code is running, then converted to UTC.
|
84
|
+
custom_stamp = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H', time: test_time)
|
85
|
+
custom_stamp
|
86
|
+
#=> "2023061514"
|
87
|
+
|
88
|
+
# Cleanup
|
89
|
+
@test_obj.id = "quantized_test_obj" # Set identifier before cleanup
|
90
|
+
@test_obj.destroy! if @test_obj
|
@@ -1,9 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# try/features/safe_dump_extended_try.rb
|
2
|
+
|
2
3
|
|
3
4
|
# These tryouts test the safe dumping functionality.
|
4
5
|
|
5
|
-
require_relative '
|
6
|
-
require_relative '
|
6
|
+
require_relative '../../lib/familia'
|
7
|
+
require_relative '../helpers/test_helpers'
|
7
8
|
|
8
9
|
## By default Familia::Base has no safe_dump_fields method
|
9
10
|
Familia::Base.respond_to?(:safe_dump_fields)
|
@@ -42,12 +43,12 @@ Customer.safe_dump_fields
|
|
42
43
|
@cust2.email = "test@example.com"
|
43
44
|
@cust2.custid = "test@example.com"
|
44
45
|
@all_safe_fields = @cust2.safe_dump.keys.sort
|
45
|
-
|
46
46
|
@all_non_safe_fields = @cust2.instance_variables.map { |el|
|
47
47
|
el.to_s[1..-1].to_sym # slice off the leading @
|
48
48
|
}.sort
|
49
|
+
# Check if any of the non-safe fields are in the safe dump (tryouts bug
|
50
|
+
# if this comment is placed right before the last line.)
|
49
51
|
p [1, all_non_safe_fields: @all_non_safe_fields]
|
50
|
-
# Check if any of the non-safe fields are in the safe dump
|
51
52
|
(@all_non_safe_fields & @all_safe_fields) - [:custid, :role, :verified, :updated, :created, :secrets_created]
|
52
53
|
#=> []
|
53
54
|
|
@@ -56,7 +57,7 @@ Bone.respond_to?(:safe_dump_fields)
|
|
56
57
|
#=> false
|
57
58
|
|
58
59
|
## Bone instances do not have safe_dump method
|
59
|
-
@bone = Bone.new(
|
60
|
+
@bone = Bone.new(token: "boneid1", name: "Rex")
|
60
61
|
@bone.respond_to?(:safe_dump)
|
61
62
|
#=> false
|
62
63
|
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# try/features/safe_dump_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
# Test SafeDump feature functionality
|
9
|
+
|
10
|
+
# Define a test class with SafeDump feature
|
11
|
+
class SafeDumpTest < Familia::Horreum
|
12
|
+
feature :safe_dump
|
13
|
+
identifier_field :id
|
14
|
+
field :id
|
15
|
+
field :name
|
16
|
+
field :email
|
17
|
+
field :secret_data
|
18
|
+
|
19
|
+
@safe_dump_fields = [
|
20
|
+
:id,
|
21
|
+
:name,
|
22
|
+
{ :display_name => ->(obj) { "#{obj.name} (#{obj.id})" } },
|
23
|
+
{ :has_email => ->(obj) { !obj.email.nil? && !obj.email.empty? } }
|
24
|
+
]
|
25
|
+
|
26
|
+
def active?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Setup test object
|
32
|
+
@test_obj = SafeDumpTest.new
|
33
|
+
@test_obj.id = "safe_test_1"
|
34
|
+
@test_obj.name = "Test User"
|
35
|
+
@test_obj.email = "test@example.com"
|
36
|
+
@test_obj.secret_data = "sensitive_info"
|
37
|
+
|
38
|
+
## Class has SafeDump methods
|
39
|
+
SafeDumpTest.respond_to?(:safe_dump_fields)
|
40
|
+
#=> true
|
41
|
+
|
42
|
+
## Class has safe_dump_field_map method
|
43
|
+
SafeDumpTest.respond_to?(:safe_dump_field_map)
|
44
|
+
#=> true
|
45
|
+
|
46
|
+
## Object has safe_dump method
|
47
|
+
@test_obj.respond_to?(:safe_dump)
|
48
|
+
#=> true
|
49
|
+
|
50
|
+
## safe_dump_fields returns field names only
|
51
|
+
fields = SafeDumpTest.safe_dump_fields
|
52
|
+
fields
|
53
|
+
#=> [:id, :name, :display_name, :has_email]
|
54
|
+
|
55
|
+
## safe_dump_field_map returns callable map
|
56
|
+
field_map = SafeDumpTest.safe_dump_field_map
|
57
|
+
field_map.keys.sort
|
58
|
+
#=> [:display_name, :has_email, :id, :name]
|
59
|
+
|
60
|
+
## safe_dump_field_map values are callable
|
61
|
+
field_map = SafeDumpTest.safe_dump_field_map
|
62
|
+
field_map[:id]
|
63
|
+
#==> _.respond_to?(:call)
|
64
|
+
|
65
|
+
## safe_dump returns hash with safe fields only
|
66
|
+
dump = @test_obj.safe_dump
|
67
|
+
dump.keys.sort
|
68
|
+
#=> [:display_name, :has_email, :id, :name]
|
69
|
+
|
70
|
+
## safe_dump includes basic field values
|
71
|
+
dump = @test_obj.safe_dump
|
72
|
+
[dump[:id], dump[:name]]
|
73
|
+
#=> ["safe_test_1", "Test User"]
|
74
|
+
|
75
|
+
## safe_dump includes computed field values
|
76
|
+
dump = @test_obj.safe_dump
|
77
|
+
dump[:display_name]
|
78
|
+
#=> "Test User (safe_test_1)"
|
79
|
+
|
80
|
+
## safe_dump includes lambda field values
|
81
|
+
dump = @test_obj.safe_dump
|
82
|
+
dump[:has_email]
|
83
|
+
#=> true
|
84
|
+
|
85
|
+
## safe_dump excludes non-whitelisted fields
|
86
|
+
dump = @test_obj.safe_dump
|
87
|
+
dump.key?(:secret_data)
|
88
|
+
#=> false
|
89
|
+
|
90
|
+
## safe_dump excludes email field not in whitelist
|
91
|
+
dump = @test_obj.safe_dump
|
92
|
+
dump.key?(:email)
|
93
|
+
#=> false
|
94
|
+
|
95
|
+
## Safe dump works with nil values
|
96
|
+
@test_obj.email = nil
|
97
|
+
dump = @test_obj.safe_dump
|
98
|
+
dump[:has_email]
|
99
|
+
#=> false
|
100
|
+
|
101
|
+
## Safe dump works with empty values
|
102
|
+
@test_obj.email = ""
|
103
|
+
dump = @test_obj.safe_dump
|
104
|
+
dump[:has_email]
|
105
|
+
#=> false
|
106
|
+
|
107
|
+
## Can define safe_dump_fields with set_safe_dump_fields
|
108
|
+
class DynamicSafeDump < Familia::Horreum
|
109
|
+
feature :safe_dump
|
110
|
+
identifier_field :id
|
111
|
+
field :id
|
112
|
+
field :data
|
113
|
+
end
|
114
|
+
|
115
|
+
DynamicSafeDump.set_safe_dump_fields(:id, :data)
|
116
|
+
DynamicSafeDump.safe_dump_fields
|
117
|
+
#=> [:id, :data]
|
118
|
+
|
119
|
+
## Class with no safe_dump_fields defined has empty array
|
120
|
+
class EmptySafeDump < Familia::Horreum
|
121
|
+
feature :safe_dump
|
122
|
+
identifier_field :id
|
123
|
+
field :id
|
124
|
+
end
|
125
|
+
|
126
|
+
EmptySafeDump.safe_dump_fields
|
127
|
+
#=> []
|
128
|
+
|
129
|
+
## Empty safe_dump returns empty hash
|
130
|
+
@empty_obj = EmptySafeDump.new
|
131
|
+
@empty_obj.id = "empty_test"
|
132
|
+
@empty_obj.safe_dump
|
133
|
+
#=> {}
|
134
|
+
|
135
|
+
# Cleanup
|
136
|
+
@test_obj.destroy! if @test_obj
|
137
|
+
@empty_obj.destroy! if @empty_obj
|
@@ -1,14 +1,17 @@
|
|
1
|
-
#
|
1
|
+
# try/helpers/test_helpers.rb
|
2
|
+
|
3
|
+
# To enable tracing and debug mode, run with the env vars set.
|
4
|
+
#
|
5
|
+
# e.g. FAMILIA_TRACE=1 FAMILIA_DEBUG=1 bundle exec try
|
2
6
|
|
3
7
|
require 'digest'
|
4
|
-
require_relative '
|
8
|
+
require_relative '../../lib/familia'
|
5
9
|
|
6
|
-
Familia.
|
7
|
-
Familia.
|
8
|
-
Familia.enable_redis_counter = true
|
10
|
+
Familia.enable_database_logging = true
|
11
|
+
Familia.enable_database_counter = true
|
9
12
|
|
10
13
|
class Bone < Familia::Horreum
|
11
|
-
|
14
|
+
identifier_field :token
|
12
15
|
field :token
|
13
16
|
field :name
|
14
17
|
list :owners
|
@@ -28,8 +31,8 @@ class Blone < Familia::Horreum
|
|
28
31
|
end
|
29
32
|
|
30
33
|
class Customer < Familia::Horreum
|
31
|
-
|
32
|
-
|
34
|
+
logical_database 15 # Use something other than the default DB
|
35
|
+
default_expiration 5.years
|
33
36
|
|
34
37
|
feature :safe_dump
|
35
38
|
#feature :expiration
|
@@ -63,13 +66,12 @@ class Customer < Familia::Horreum
|
|
63
66
|
|
64
67
|
counter :secrets_created
|
65
68
|
|
66
|
-
|
69
|
+
identifier_field :custid
|
67
70
|
|
68
71
|
field :custid
|
69
72
|
field :sessid
|
70
73
|
field :email
|
71
74
|
field :role
|
72
|
-
field :key
|
73
75
|
field :name
|
74
76
|
field :passphrase_encryption
|
75
77
|
field :passphrase
|
@@ -96,49 +98,24 @@ end
|
|
96
98
|
@c.custid = "d@example.com"
|
97
99
|
|
98
100
|
class Session < Familia::Horreum
|
99
|
-
|
100
|
-
|
101
|
+
logical_database 14 # don't use Onetime's default DB
|
102
|
+
default_expiration 180.minutes
|
101
103
|
|
102
|
-
|
104
|
+
identifier_field :sessid
|
103
105
|
|
104
106
|
field :sessid
|
105
107
|
field :shrimp
|
106
108
|
field :custid
|
107
109
|
field :useragent
|
108
|
-
field :key
|
109
110
|
field :authenticated
|
110
111
|
field :ipaddress
|
111
112
|
field :created
|
112
113
|
field :updated
|
113
114
|
|
114
|
-
def
|
115
|
-
|
116
|
-
|
117
|
-
end
|
118
|
-
|
119
|
-
# The external identifier is used by the rate limiter to estimate a unique
|
120
|
-
# client. We can't use the session ID b/c the request agent can choose to
|
121
|
-
# not send cookies, or the user can clear their cookies (in both cases the
|
122
|
-
# session ID would change which would circumvent the rate limiter). The
|
123
|
-
# external identifier is a hash of the IP address and the customer ID
|
124
|
-
# which means that anonymous users from the same IP address are treated
|
125
|
-
# as the same client (as far as the limiter is concerned). Not ideal.
|
126
|
-
#
|
127
|
-
# To put it another way, the risk of colliding external identifiers is
|
128
|
-
# acceptable for the rate limiter, but not for the session data. Acceptable
|
129
|
-
# b/c the rate limiter is a temporary measure to prevent abuse, and the
|
130
|
-
# worse case scenario is that a user is rate limited when they shouldn't be.
|
131
|
-
# The session data is permanent and must be kept separate to avoid leaking
|
132
|
-
# data between users.
|
133
|
-
def external_identifier
|
134
|
-
elements = []
|
135
|
-
elements << ipaddress || 'UNKNOWNIP'
|
136
|
-
elements << custid || 'anon'
|
137
|
-
@external_identifier ||= Familia.generate_sha_hash(elements)
|
138
|
-
Familia.ld "[Session.external_identifier] sess identifier input: #{elements.inspect} (result: #{@external_identifier})"
|
139
|
-
@external_identifier
|
115
|
+
def save
|
116
|
+
self.sessid ||= Familia.generate_id # Only generates when persisting
|
117
|
+
super
|
140
118
|
end
|
141
|
-
|
142
119
|
end
|
143
120
|
@s = Session.new
|
144
121
|
|
@@ -146,11 +123,11 @@ class CustomDomain < Familia::Horreum
|
|
146
123
|
|
147
124
|
feature :expiration
|
148
125
|
|
149
|
-
class_sorted_set :values
|
150
|
-
|
151
|
-
identifier :derive_id
|
126
|
+
class_sorted_set :values
|
152
127
|
|
128
|
+
identifier_field :generate_id
|
153
129
|
|
130
|
+
field :domainid
|
154
131
|
field :display_domain
|
155
132
|
field :custid
|
156
133
|
field :base_domain
|
@@ -158,7 +135,6 @@ class CustomDomain < Familia::Horreum
|
|
158
135
|
field :trd
|
159
136
|
field :tld
|
160
137
|
field :sld
|
161
|
-
# No :key field (so we can test hte behaviour in Horreum#initialize)
|
162
138
|
field :txt_validation_host
|
163
139
|
field :txt_validation_value
|
164
140
|
field :status
|
@@ -167,18 +143,8 @@ class CustomDomain < Familia::Horreum
|
|
167
143
|
field :created
|
168
144
|
field :updated
|
169
145
|
field :_original_value
|
170
|
-
|
171
|
-
# Derive a unique identifier for the object based on the display domain and
|
172
|
-
# the customer ID. This is used to ensure that the same domain can't be
|
173
|
-
# added twice by the same customer while avoiding collisions between customers.
|
174
|
-
def derive_id
|
175
|
-
elements = [
|
176
|
-
display_domain,
|
177
|
-
custid
|
178
|
-
]
|
179
|
-
Familia.generate_sha_hash(*elements).slice(0, 8)
|
180
|
-
end
|
181
146
|
end
|
147
|
+
|
182
148
|
@d = CustomDomain.new
|
183
149
|
@d.display_domain = "example.com"
|
184
150
|
@d.custid = @c.custid
|
@@ -188,12 +154,11 @@ class Limiter < Familia::Horreum
|
|
188
154
|
feature :expiration
|
189
155
|
feature :quantization
|
190
156
|
|
191
|
-
|
192
|
-
|
157
|
+
identifier_field :name
|
158
|
+
default_expiration 30.minutes
|
193
159
|
field :name
|
194
|
-
# No :key field (so we can test hte behaviour in Horreum#initialize)
|
195
160
|
|
196
|
-
string :counter, :
|
161
|
+
string :counter, :default_expiration => 1.hour, :quantize => [10.minutes, '%H:%M', 1302468980]
|
197
162
|
|
198
163
|
def identifier
|
199
164
|
@name
|
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# try/horreum/base_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
3
5
|
|
4
6
|
Familia.debug = false
|
5
7
|
|
@@ -8,6 +10,9 @@ Familia.debug = false
|
|
8
10
|
@hashkey = Familia::HashKey.new 'tryouts-27'
|
9
11
|
|
10
12
|
## Customer passed as value is returned as string identifier, on assignment as string
|
13
|
+
## TODO: Revisit these @identifier testcases b/c I think we don't want to be setting
|
14
|
+
## the @identifier instance var anymore since identifer_field should only take field
|
15
|
+
## names now (and might be removed altogether).
|
11
16
|
@hashkey["test1"] = @customer.identifier
|
12
17
|
#=> @identifier
|
13
18
|
|
@@ -48,7 +53,7 @@ Familia.debug = false
|
|
48
53
|
#=> true
|
49
54
|
|
50
55
|
## Horreum object fields have a fast attribute method (1 of 2)
|
51
|
-
Familia.trace :LOAD, @customer.
|
56
|
+
Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
|
52
57
|
@customer.name! 'Jane Doe'
|
53
58
|
#=> 0
|
54
59
|
|
@@ -68,26 +73,46 @@ Familia.trace :LOAD, @customer.redis, @customer.redisuri, caller if Familia.debu
|
|
68
73
|
#=> true
|
69
74
|
|
70
75
|
## All horrerum objects have a key field
|
71
|
-
@customer.
|
76
|
+
@customer.identifier
|
72
77
|
#=> @identifier
|
73
78
|
|
74
79
|
## Even ones that didn't define it
|
75
|
-
|
76
|
-
|
80
|
+
class NoIdentifierClass < Familia::Horreum
|
81
|
+
field :name
|
82
|
+
end
|
83
|
+
@no_id = NoIdentifierClass.new name: "test"
|
84
|
+
@no_id.identifier
|
77
85
|
#=> nil
|
78
86
|
|
79
87
|
## We can call #identifier directly if we want to "lasy load" the unique identifier
|
88
|
+
@cd = CustomDomain.new display_domain: "www.example.com", custid: "domain-test@example.com"
|
80
89
|
@cd.identifier
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
#=:> String
|
91
|
+
#=/=> _.empty?
|
92
|
+
#==> _.size > 16
|
93
|
+
#=~>/\A[0-9a-z]+\z/
|
94
|
+
|
95
|
+
## The identifier is now memoized (same value each time)
|
96
|
+
@cd_first_call = @cd.identifier
|
97
|
+
@cd_second_call = @cd.identifier
|
98
|
+
@cd_first_call == @cd_second_call
|
99
|
+
#=> true
|
86
100
|
|
87
101
|
## But once we save
|
88
102
|
@cd.save
|
89
103
|
#=> true
|
90
104
|
|
91
|
-
## The key
|
92
|
-
@cd.
|
93
|
-
|
105
|
+
## The key has been set now that the instance has been saved
|
106
|
+
@cd.identifier
|
107
|
+
#=:> String
|
108
|
+
#=/=> _.empty?
|
109
|
+
#==> _.size > 16
|
110
|
+
#=~>/\A[0-9a-z]+\z/
|
111
|
+
|
112
|
+
## Array-based identifiers are no longer supported and raise clear errors at class definition time
|
113
|
+
class ArrayIdentifierTest < Familia::Horreum
|
114
|
+
identifier_field [:token, :name] # This should raise an error immediately
|
115
|
+
field :token
|
116
|
+
field :name
|
117
|
+
end
|
118
|
+
#=!> Familia::Problem
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test Horreum class methods
|
4
|
+
group "Horreum Class Methods"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@user_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :email
|
9
|
+
field :name
|
10
|
+
field :age
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
try "create factory method with existence checking" do
|
15
|
+
user = @user_class.create(email: "test@example.com", name: "Test")
|
16
|
+
exists = @user_class.exists?("test@example.com")
|
17
|
+
|
18
|
+
user.is_a?(@user_class) && exists
|
19
|
+
ensure
|
20
|
+
@user_class.destroy!("test@example.com")
|
21
|
+
end
|
22
|
+
|
23
|
+
try "multiget retrieves multiple objects" do
|
24
|
+
@user_class.create(email: "user1@example.com", name: "User1")
|
25
|
+
@user_class.create(email: "user2@example.com", name: "User2")
|
26
|
+
|
27
|
+
users = @user_class.multiget("user1@example.com", "user2@example.com")
|
28
|
+
|
29
|
+
users.length == 2 && users.all? { |u| u.is_a?(@user_class) }
|
30
|
+
ensure
|
31
|
+
@user_class.destroy!("user1@example.com", "user2@example.com")
|
32
|
+
end
|
33
|
+
|
34
|
+
try "find_keys returns matching Redis keys" do
|
35
|
+
@user_class.create(email: "test@example.com", name: "Test")
|
36
|
+
keys = @user_class.find_keys
|
37
|
+
|
38
|
+
keys.any? { |key| key.include?("test@example.com") }
|
39
|
+
ensure
|
40
|
+
@user_class.destroy!("test@example.com")
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test Horreum Redis commands
|
4
|
+
group "Horreum Commands"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@user_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :email
|
9
|
+
field :name
|
10
|
+
field :score
|
11
|
+
end
|
12
|
+
@user = @user_class.new(email: "test@example.com", name: "Test")
|
13
|
+
@user.save
|
14
|
+
end
|
15
|
+
|
16
|
+
try "hget/hset operations" do
|
17
|
+
@user.hset("name", "Updated")
|
18
|
+
@user.hget("name") == "Updated"
|
19
|
+
end
|
20
|
+
|
21
|
+
try "increment/decrement operations" do
|
22
|
+
@user.hset("score", "100")
|
23
|
+
@user.incr("score", 10)
|
24
|
+
@user.hget("score").to_i == 110 &&
|
25
|
+
@user.decr("score", 5) == 105
|
26
|
+
end
|
27
|
+
|
28
|
+
try "field existence and removal" do
|
29
|
+
@user.hset("temp_field", "value")
|
30
|
+
exists_before = @user.key?("temp_field")
|
31
|
+
@user.remove_field("temp_field")
|
32
|
+
exists_after = @user.key?("temp_field")
|
33
|
+
|
34
|
+
exists_before && !exists_after
|
35
|
+
end
|
36
|
+
|
37
|
+
try "bulk field operations" do
|
38
|
+
fields = @user.hkeys
|
39
|
+
values = @user.hvals
|
40
|
+
all_data = @user.hgetall
|
41
|
+
|
42
|
+
fields.is_a?(Array) &&
|
43
|
+
values.is_a?(Array) &&
|
44
|
+
all_data.is_a?(Hash)
|
45
|
+
end
|
46
|
+
|
47
|
+
cleanup do
|
48
|
+
@user&.delete!
|
49
|
+
end
|