familia 2.0.0.pre18 → 2.0.0.pre19
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/CHANGELOG.rst +58 -6
- data/CLAUDE.md +34 -9
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +39 -0
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +3 -2
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/database_commands.rb +7 -4
- data/lib/familia/data_type/serialization.rb +4 -0
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +129 -10
- data/lib/familia/horreum/definition.rb +2 -1
- data/lib/familia/horreum/management.rb +21 -15
- data/lib/familia/horreum/persistence.rb +190 -66
- data/lib/familia/horreum/serialization.rb +3 -0
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +31 -12
- data/lib/familia/logging.rb +2 -5
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/middleware/database_logger.rb +76 -5
- data/try/edge_cases/string_coercion_try.rb +4 -4
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/relationships/indexing_try.rb +28 -4
- data/try/features/relationships/relationships_api_changes_try.rb +4 -4
- data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
- data/try/integration/connection/operation_mode_guards_try.rb +1 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
- data/try/integration/create_method_try.rb +22 -22
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/integration/models/customer_safe_dump_try.rb +5 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/persistence_operations_try.rb +162 -10
- data/try/unit/data_types/boolean_try.rb +1 -1
- data/try/unit/data_types/string_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/class_methods_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +4 -4
- data/try/unit/horreum/serialization_try.rb +2 -2
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- metadata +14 -2
@@ -0,0 +1,197 @@
|
|
1
|
+
# Migrating Guide: v2.0.0-pre19
|
2
|
+
|
3
|
+
This version introduces significant improvements to Familia's database operations, making them more atomic, reliable, and consistent with Rails conventions. The changes include enhanced error handling, optimistic locking support, and breaking API changes.
|
4
|
+
|
5
|
+
## Breaking Changes
|
6
|
+
|
7
|
+
### Management.create → create!
|
8
|
+
|
9
|
+
**What Changed:**
|
10
|
+
|
11
|
+
The `create` class method has been renamed to `create!` to follow Rails conventions and indicate that it raises exceptions on failure.
|
12
|
+
|
13
|
+
**Before:**
|
14
|
+
```ruby
|
15
|
+
user = User.create(email: "test@example.com")
|
16
|
+
```
|
17
|
+
|
18
|
+
**After:**
|
19
|
+
```ruby
|
20
|
+
user = User.create!(email: "test@example.com")
|
21
|
+
```
|
22
|
+
|
23
|
+
**Why This Matters:**
|
24
|
+
|
25
|
+
- Follows Rails naming conventions where `!` methods raise exceptions
|
26
|
+
- Makes it clear that `CreationError` will be raised if object already exists
|
27
|
+
- Prevents silent failures in object creation
|
28
|
+
|
29
|
+
### save_if_not_exists → save_if_not_exists!
|
30
|
+
|
31
|
+
**What Changed:**
|
32
|
+
|
33
|
+
The `save_if_not_exists` method has been renamed to `save_if_not_exists!` and now includes optimistic locking with automatic retry logic.
|
34
|
+
|
35
|
+
**Before:**
|
36
|
+
```ruby
|
37
|
+
success = user.save_if_not_exists
|
38
|
+
```
|
39
|
+
|
40
|
+
**After:**
|
41
|
+
```ruby
|
42
|
+
success = user.save_if_not_exists!
|
43
|
+
```
|
44
|
+
|
45
|
+
**Behavior Changes:**
|
46
|
+
- Now uses Redis WATCH/MULTI/EXEC for optimistic locking
|
47
|
+
- Automatically retries up to 3 times on `OptimisticLockError`
|
48
|
+
- Raises `RecordExistsError` if object already exists
|
49
|
+
- More atomic and thread-safe
|
50
|
+
|
51
|
+
## Enhanced Error Handling
|
52
|
+
|
53
|
+
### New Error Hierarchy
|
54
|
+
|
55
|
+
**What's New:**
|
56
|
+
|
57
|
+
A structured error hierarchy provides better error categorization:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
Familia::Problem # Base class
|
61
|
+
├── Familia::PersistenceError # Redis/database errors
|
62
|
+
│ ├── Familia::NonUniqueKey
|
63
|
+
│ ├── Familia::OptimisticLockError
|
64
|
+
│ ├── Familia::OperationModeError
|
65
|
+
│ ├── Familia::NoConnectionAvailable
|
66
|
+
│ ├── Familia::NotFound
|
67
|
+
│ └── Familia::NotConnected
|
68
|
+
└── Familia::HorreumError # Model-related errors
|
69
|
+
├── Familia::CreationError
|
70
|
+
├── Familia::NoIdentifier
|
71
|
+
├── Familia::FieldTypeError
|
72
|
+
├── Familia::AutoloadError
|
73
|
+
├── Familia::SerializerError
|
74
|
+
├── Familia::UnknownFieldError
|
75
|
+
└── Familia::NotDistinguishableError
|
76
|
+
```
|
77
|
+
|
78
|
+
**Migration:**
|
79
|
+
|
80
|
+
Update exception handling to use specific error classes:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# Before
|
84
|
+
rescue Familia::Problem => e
|
85
|
+
# Handle all errors
|
86
|
+
|
87
|
+
# After - More granular handling
|
88
|
+
rescue Familia::CreationError => e
|
89
|
+
# Handle creation failures specifically
|
90
|
+
rescue Familia::OptimisticLockError => e
|
91
|
+
# Handle concurrent modification
|
92
|
+
rescue Familia::PersistenceError => e
|
93
|
+
# Handle database-related errors
|
94
|
+
```
|
95
|
+
|
96
|
+
### New Exception Types
|
97
|
+
|
98
|
+
- **`CreationError`**: Raised when object creation fails (replaces generic errors)
|
99
|
+
- **`OptimisticLockError`**: Raised when WATCH fails due to concurrent modification
|
100
|
+
- **`UnknownFieldError`**: Raised when referencing non-existent fields
|
101
|
+
|
102
|
+
## Database Operation Improvements
|
103
|
+
|
104
|
+
### Atomic Save Operations
|
105
|
+
|
106
|
+
**What Changed:**
|
107
|
+
|
108
|
+
The `save` method now uses a single Redis transaction for complete atomicity:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# All operations now happen atomically:
|
112
|
+
# 1. Save all fields (HMSET)
|
113
|
+
# 2. Set expiration (EXPIRE)
|
114
|
+
# 3. Update indexes
|
115
|
+
# 4. Add to instances collection
|
116
|
+
```
|
117
|
+
|
118
|
+
**Benefits:**
|
119
|
+
- Eliminates race conditions during save operations
|
120
|
+
- Ensures data consistency across related operations
|
121
|
+
- Better performance with fewer round trips
|
122
|
+
|
123
|
+
### Enhanced Timestamp Precision
|
124
|
+
|
125
|
+
**What Changed:**
|
126
|
+
|
127
|
+
Created/updated timestamps now use float values instead of integers for higher precision:
|
128
|
+
|
129
|
+
**Before:**
|
130
|
+
```ruby
|
131
|
+
user.created # => 1697234567 (integer seconds)
|
132
|
+
```
|
133
|
+
|
134
|
+
**After:**
|
135
|
+
```ruby
|
136
|
+
user.created # => 1697234567.123 (float with milliseconds)
|
137
|
+
```
|
138
|
+
|
139
|
+
**Migration:**
|
140
|
+
|
141
|
+
No code changes needed. Existing integer timestamps continue to work.
|
142
|
+
|
143
|
+
## Redis Command Enhancements
|
144
|
+
|
145
|
+
### New Commands Available
|
146
|
+
|
147
|
+
Added support for optimistic locking commands:
|
148
|
+
- `watch(key)` - Watch key for changes
|
149
|
+
- `unwatch()` - Remove all watches
|
150
|
+
- `discard()` - Discard queued commands
|
151
|
+
|
152
|
+
### Improved Command Logging
|
153
|
+
|
154
|
+
Database command logging now includes:
|
155
|
+
- Structured format for better readability
|
156
|
+
- Pipelined operation tracking
|
157
|
+
- Transaction boundary markers
|
158
|
+
- Command timing information
|
159
|
+
|
160
|
+
## Terminology Updates
|
161
|
+
|
162
|
+
**What Changed:**
|
163
|
+
|
164
|
+
Standardized on "pipelined" terminology throughout (previously mixed "pipeline"/"pipelined").
|
165
|
+
|
166
|
+
**Files Affected:**
|
167
|
+
- Method names now consistently use "pipelined"
|
168
|
+
- Documentation updated to match Redis terminology
|
169
|
+
- Log messages standardized
|
170
|
+
|
171
|
+
**Migration:**
|
172
|
+
|
173
|
+
No code changes needed - this was an internal consistency improvement.
|
174
|
+
|
175
|
+
## Recommended Actions
|
176
|
+
|
177
|
+
1. **Update method calls:**
|
178
|
+
```ruby
|
179
|
+
# Replace all instances
|
180
|
+
Model.create(...) → Model.create!(...)
|
181
|
+
obj.save_if_not_exists → obj.save_if_not_exists!
|
182
|
+
```
|
183
|
+
|
184
|
+
2. **Review error handling:**
|
185
|
+
```ruby
|
186
|
+
# Consider more specific error handling
|
187
|
+
rescue Familia::CreationError
|
188
|
+
rescue Familia::OptimisticLockError
|
189
|
+
```
|
190
|
+
|
191
|
+
3. **Test concurrent operations:**
|
192
|
+
- The new optimistic locking provides better concurrency handling
|
193
|
+
- Verify your application handles `OptimisticLockError` appropriately
|
194
|
+
|
195
|
+
4. **Review logging:**
|
196
|
+
- Enhanced database command logging may affect log volume
|
197
|
+
- Adjust log levels if needed for production environments
|
@@ -0,0 +1,281 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/datatype_standalone.rb
|
3
|
+
|
4
|
+
# Demonstration: Familia::StringKey for Session Storage with Atomic Transactions
|
5
|
+
#
|
6
|
+
# This example shows how to use Familia's DataType classes independently
|
7
|
+
# without inheriting from Familia::Horreum. It implements a Rack-compatible
|
8
|
+
# session store using Familia::StringKey for secure, TTL-managed storage.
|
9
|
+
#
|
10
|
+
# Key Familia Features Demonstrated:
|
11
|
+
# - Standalone DataType usage (no parent model required)
|
12
|
+
# - Atomic transactions for multi-operation consistency
|
13
|
+
# - TTL management for automatic expiration
|
14
|
+
# - JSON serialization for complex data structures
|
15
|
+
# - Direct Redis access through DataType objects
|
16
|
+
|
17
|
+
require 'rack/session/abstract/id'
|
18
|
+
require 'securerandom'
|
19
|
+
|
20
|
+
require 'base64'
|
21
|
+
require 'openssl'
|
22
|
+
|
23
|
+
# Load local development version of Familia (not the gem)
|
24
|
+
begin
|
25
|
+
require_relative '../lib/familia'
|
26
|
+
rescue LoadError
|
27
|
+
# Fall back to installed gem
|
28
|
+
require 'familia'
|
29
|
+
end
|
30
|
+
|
31
|
+
# SecureSessionStore - a rack-session compatible session store using Familia::StringKey
|
32
|
+
#
|
33
|
+
# Usage:
|
34
|
+
# ruby examples/datatype_standalone.rb
|
35
|
+
# # Or in your Rack app:
|
36
|
+
# use SecureSessionStore, secret: 'your-secret-key', expire_after: 3600
|
37
|
+
#
|
38
|
+
# @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/abstract/id.rb
|
39
|
+
# @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/encryptor.rb
|
40
|
+
#
|
41
|
+
class SecureSessionStore < Rack::Session::Abstract::PersistedSecure
|
42
|
+
unless defined?(DEFAULT_OPTIONS)
|
43
|
+
DEFAULT_OPTIONS = {
|
44
|
+
key: 'project.session',
|
45
|
+
expire_after: 86_400, # 24 hours default
|
46
|
+
namespace: 'session',
|
47
|
+
sidbits: 256, # Required by Rack::Session::Abstract::Persisted
|
48
|
+
dbclient: nil,
|
49
|
+
}.freeze
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :dbclient
|
53
|
+
|
54
|
+
def initialize(app, options = {})
|
55
|
+
# Require a secret for security
|
56
|
+
raise ArgumentError, 'Secret required for secure sessions' unless options[:secret]
|
57
|
+
|
58
|
+
# Merge options with defaults
|
59
|
+
options = DEFAULT_OPTIONS.merge(options)
|
60
|
+
|
61
|
+
# Configure Familia connection if redis_uri provided
|
62
|
+
@dbclient = options[:dbclient] || Familia.dbclient
|
63
|
+
|
64
|
+
super
|
65
|
+
|
66
|
+
@secret = options[:secret]
|
67
|
+
@expire_after = options[:expire_after]
|
68
|
+
@namespace = options[:namespace] || 'session'
|
69
|
+
|
70
|
+
# Derive different keys for different purposes
|
71
|
+
@hmac_key = derive_key('hmac')
|
72
|
+
@encryption_key = derive_key('encryption')
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Create a StringKey instance for a session ID
|
78
|
+
def get_stringkey(sid)
|
79
|
+
return nil if sid.to_s.empty?
|
80
|
+
|
81
|
+
key = Familia.join(@namespace, sid)
|
82
|
+
Familia::StringKey.new(key,
|
83
|
+
ttl: @expire_after,
|
84
|
+
default: nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete_session(_request, sid, _options)
|
88
|
+
# Extract string ID from SessionId object if needed
|
89
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
90
|
+
|
91
|
+
get_stringkey(sid_string)&.del
|
92
|
+
|
93
|
+
generate_sid
|
94
|
+
end
|
95
|
+
|
96
|
+
def valid_session_id?(sid)
|
97
|
+
return false if sid.to_s.empty?
|
98
|
+
return false unless sid.match?(/\A[a-f0-9]{64,}\z/)
|
99
|
+
|
100
|
+
# Additional security checks could go here
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_hmac?(data, hmac)
|
105
|
+
expected = compute_hmac(data)
|
106
|
+
return false unless hmac.is_a?(String) && expected.is_a?(String) && hmac.bytesize == expected.bytesize
|
107
|
+
|
108
|
+
Rack::Utils.secure_compare(expected, hmac)
|
109
|
+
end
|
110
|
+
|
111
|
+
def derive_key(purpose)
|
112
|
+
OpenSSL::HMAC.hexdigest('SHA256', @secret, "session-#{purpose}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def compute_hmac(data)
|
116
|
+
OpenSSL::HMAC.hexdigest('SHA256', @hmac_key, data)
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_session(_request, sid)
|
120
|
+
# Parent class already extracts sid from cookies
|
121
|
+
# sid may be a SessionId object or nil
|
122
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
123
|
+
|
124
|
+
# Only generate new sid if none provided or invalid
|
125
|
+
return [generate_sid, {}] unless sid_string && valid_session_id?(sid_string)
|
126
|
+
|
127
|
+
begin
|
128
|
+
stringkey = get_stringkey(sid_string)
|
129
|
+
stored_data = stringkey.value if stringkey
|
130
|
+
|
131
|
+
# If no data stored, return empty session
|
132
|
+
return [sid, {}] unless stored_data
|
133
|
+
|
134
|
+
# Verify HMAC before deserializing
|
135
|
+
data, hmac = stored_data.split('--', 2)
|
136
|
+
|
137
|
+
# If no HMAC or invalid format, create new session
|
138
|
+
unless hmac && valid_hmac?(data, hmac)
|
139
|
+
# Session tampered with - create new session
|
140
|
+
return [generate_sid, {}]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Decode and parse the session data
|
144
|
+
session_data = Familia::JsonSerializer.parse(Base64.decode64(data))
|
145
|
+
|
146
|
+
[sid, session_data]
|
147
|
+
rescue Familia::PersistenceError => e
|
148
|
+
# Log error in development/debugging
|
149
|
+
Familia.ld "[Session] Error reading session #{sid_string}: #{e.message}"
|
150
|
+
|
151
|
+
# Return new session on any error
|
152
|
+
[generate_sid, {}]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def write_session(_request, sid, session_data, _options)
|
157
|
+
# Extract string ID from SessionId object if needed
|
158
|
+
sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
|
159
|
+
|
160
|
+
# Serialize and sign the data
|
161
|
+
encoded = Base64.encode64(Familia::JsonSerializer.dump(session_data)).delete("\n")
|
162
|
+
hmac = compute_hmac(encoded)
|
163
|
+
signed_data = "#{encoded}--#{hmac}"
|
164
|
+
|
165
|
+
# Get or create StringKey for this session
|
166
|
+
stringkey = get_stringkey(sid_string)
|
167
|
+
|
168
|
+
# ATOMIC TRANSACTION: Ensures both operations succeed or both fail
|
169
|
+
#
|
170
|
+
# Before DataType transaction support (PR #160), these operations were not atomic:
|
171
|
+
# stringkey.set(signed_data)
|
172
|
+
# stringkey.update_expiration(expiration: @expire_after)
|
173
|
+
#
|
174
|
+
# With transaction support, we guarantee atomicity - critical for session storage
|
175
|
+
# where partial writes could lead to sessions without TTL (memory leaks) or
|
176
|
+
# expired sessions with stale data (security issues).
|
177
|
+
#
|
178
|
+
# RECOMMENDED PATTERN: Use DataType methods inside transaction blocks
|
179
|
+
# The transaction block automatically handles the atomic MULTI/EXEC wrapping.
|
180
|
+
# DataType methods handle key generation and provide clean, expressive syntax.
|
181
|
+
stringkey.transaction do
|
182
|
+
stringkey.set(signed_data)
|
183
|
+
stringkey.update_expiration(expiration: @expire_after) if @expire_after&.positive?
|
184
|
+
end
|
185
|
+
|
186
|
+
# ADVANCED: The block yields the Redis connection for low-level access when needed
|
187
|
+
# This is useful for operations that require direct Redis command access or
|
188
|
+
# when working with multiple DataTypes in a single transaction.
|
189
|
+
#
|
190
|
+
# stringkey.transaction do |conn|
|
191
|
+
# conn.set(stringkey.dbkey, signed_data)
|
192
|
+
# conn.expire(stringkey.dbkey, @expire_after) if @expire_after&.positive?
|
193
|
+
# end
|
194
|
+
|
195
|
+
# Return the original sid (may be SessionId object)
|
196
|
+
sid
|
197
|
+
rescue Familia::PersistenceError => e
|
198
|
+
# Log error in development/debugging
|
199
|
+
Familia.ld "[Session] Error writing session #{sid_string}: #{e.message}"
|
200
|
+
|
201
|
+
# Return false to indicate failure
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
# Clean up expired sessions (optional, can be called periodically)
|
206
|
+
def cleanup_expired_sessions
|
207
|
+
# This would typically be handled by Redis TTL automatically
|
208
|
+
# but you could implement manual cleanup if needed
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Demo application showing session store in action
|
213
|
+
class DemoApp
|
214
|
+
def initialize
|
215
|
+
@store = SecureSessionStore.new(
|
216
|
+
proc { |_env| [200, {}, ['Demo App']] },
|
217
|
+
secret: 'demo-secret-key-change-in-production',
|
218
|
+
expire_after: 300, # 5 minutes for demo
|
219
|
+
)
|
220
|
+
end
|
221
|
+
|
222
|
+
def call(env)
|
223
|
+
puts "\n=== Familia::StringKey Session Demo ==="
|
224
|
+
|
225
|
+
# Mock Rack environment
|
226
|
+
env['rack.session'] ||= {}
|
227
|
+
env['HTTP_COOKIE'] ||= ''
|
228
|
+
|
229
|
+
# Simulate session operations
|
230
|
+
session_id = SecureRandom.hex(32)
|
231
|
+
session_data = {
|
232
|
+
'user_id' => '12345',
|
233
|
+
'username' => 'demo_user',
|
234
|
+
'login_time' => Time.now.to_i,
|
235
|
+
'preferences' => { 'theme' => 'dark', 'lang' => 'en' },
|
236
|
+
}
|
237
|
+
|
238
|
+
puts 'Writing session data...'
|
239
|
+
result = @store.send(:write_session, nil, session_id, session_data, {})
|
240
|
+
puts " Result: #{result ? 'Success' : 'Failed'}"
|
241
|
+
|
242
|
+
puts "\nReading session data..."
|
243
|
+
found_id, found_data = @store.send(:find_session, nil, session_id)
|
244
|
+
puts " Session ID: #{found_id}"
|
245
|
+
puts " Data: #{found_data}"
|
246
|
+
|
247
|
+
puts "\nDeleting session..."
|
248
|
+
@store.send(:delete_session, nil, session_id, {})
|
249
|
+
|
250
|
+
puts "\nVerifying deletion..."
|
251
|
+
deleted_id, deleted_data = @store.send(:find_session, nil, session_id)
|
252
|
+
puts " Data after deletion: #{deleted_data}"
|
253
|
+
puts " New session ID: #{deleted_id == session_id ? 'Same' : 'Generated'}"
|
254
|
+
|
255
|
+
puts "\n✅ Demo complete!"
|
256
|
+
puts "\nKey Familia Features Used:"
|
257
|
+
puts '• Familia::StringKey for typed Redis storage'
|
258
|
+
puts '• Automatic TTL management'
|
259
|
+
puts '• Direct Redis operations (set, get, del)'
|
260
|
+
puts '• JSON serialization support'
|
261
|
+
puts '• No Horreum inheritance required'
|
262
|
+
|
263
|
+
[200, { 'Content-Type' => 'text/plain' }, ['Familia StringKey Demo - Check console output']]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Run demo if executed directly
|
268
|
+
if __FILE__ == $0
|
269
|
+
# Ensure Redis is available
|
270
|
+
begin
|
271
|
+
Familia.dbclient.ping
|
272
|
+
rescue Familia::PersistenceError => e
|
273
|
+
puts "❌ Redis connection failed: #{e.message}"
|
274
|
+
puts ' Please ensure Redis is running on localhost:6379'
|
275
|
+
exit 1
|
276
|
+
end
|
277
|
+
|
278
|
+
# Run the demo
|
279
|
+
app = DemoApp.new
|
280
|
+
app.call({})
|
281
|
+
end
|