familia 2.0.0.pre23 → 2.0.0.pre25

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.
@@ -0,0 +1,248 @@
1
+ # try/edge_cases/find_by_dbkey_race_condition_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Test race condition handling in find_by_dbkey where a key can expire
6
+ # between the EXISTS check and HGETALL retrieval. Also tests lazy cleanup
7
+ # of stale instances entries.
8
+ #
9
+ # The race condition scenario:
10
+ # 1. EXISTS check passes (key exists)
11
+ # 2. Key expires via TTL (or is deleted) before HGETALL
12
+ # 3. HGETALL returns empty hash {}
13
+ # 4. Without fix: instantiate_from_hash({}) creates object with nil identifier
14
+ # 5. With fix: returns nil and cleans up stale instances entry
15
+
16
+ require_relative '../support/helpers/test_helpers'
17
+
18
+ RaceConditionUser = Class.new(Familia::Horreum) do
19
+ identifier_field :user_id
20
+ field :user_id
21
+ field :name
22
+ field :email
23
+ end
24
+
25
+ RaceConditionSession = Class.new(Familia::Horreum) do
26
+ identifier_field :session_id
27
+ field :session_id
28
+ field :data
29
+ feature :expiration
30
+ default_expiration 300
31
+ end
32
+
33
+ # --- Empty Hash Handling Tests ---
34
+
35
+ ## find_by_dbkey returns nil for empty hash when check_exists: true
36
+ # Simulate race condition: add stale entry to instances, then try to load
37
+ RaceConditionUser.instances.add('stale_user_1', Familia.now)
38
+ initial_count = RaceConditionUser.instances.size
39
+ result = RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('stale_user_1'), check_exists: true)
40
+ result
41
+ #=> nil
42
+
43
+ ## find_by_dbkey returns nil for empty hash when check_exists: false
44
+ RaceConditionUser.instances.add('stale_user_2', Familia.now)
45
+ result = RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('stale_user_2'), check_exists: false)
46
+ result
47
+ #=> nil
48
+
49
+ ## find_by_dbkey handles both check_exists modes consistently for non-existent keys
50
+ result_true = RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('nonexistent_1'), check_exists: true)
51
+ result_false = RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('nonexistent_2'), check_exists: false)
52
+ [result_true, result_false]
53
+ #=> [nil, nil]
54
+
55
+ # --- Lazy Cleanup Tests ---
56
+
57
+ ## lazy cleanup removes stale entry from instances when loading fails
58
+ RaceConditionUser.instances.clear
59
+ RaceConditionUser.instances.add('phantom_user_1', Familia.now)
60
+ before_count = RaceConditionUser.instances.size
61
+ RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('phantom_user_1'))
62
+ after_count = RaceConditionUser.instances.size
63
+ [before_count, after_count]
64
+ #=> [1, 0]
65
+
66
+ ## lazy cleanup handles multiple stale entries
67
+ RaceConditionUser.instances.clear
68
+ RaceConditionUser.instances.add('phantom_a', Familia.now)
69
+ RaceConditionUser.instances.add('phantom_b', Familia.now)
70
+ RaceConditionUser.instances.add('phantom_c', Familia.now)
71
+ RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('phantom_a'))
72
+ RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('phantom_b'))
73
+ remaining = RaceConditionUser.instances.size
74
+ remaining
75
+ #=> 1
76
+
77
+ ## lazy cleanup only removes the specific stale entry
78
+ RaceConditionUser.instances.clear
79
+ real_user = RaceConditionUser.new(user_id: 'real_user_1', name: 'Real', email: 'real@example.com')
80
+ real_user.save
81
+ RaceConditionUser.instances.add('phantom_mixed', Familia.now)
82
+ before = RaceConditionUser.instances.members.sort
83
+ RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('phantom_mixed'))
84
+ after = RaceConditionUser.instances.members.sort
85
+ real_user.destroy!
86
+ [before.include?('phantom_mixed'), before.include?('real_user_1'), after.include?('phantom_mixed'), after.include?('real_user_1')]
87
+ #=> [true, true, false, true]
88
+
89
+ # --- Race Condition Simulation Tests ---
90
+
91
+ ## simulated race: key deleted between conceptual EXISTS and actual load
92
+ # This simulates what happens when a key expires between EXISTS and HGETALL
93
+ user = RaceConditionUser.new(user_id: 'race_user_1', name: 'Race', email: 'race@example.com')
94
+ user.save
95
+ dbkey = RaceConditionUser.dbkey('race_user_1')
96
+
97
+ # Verify key exists
98
+ exists_before = Familia.dbclient.exists(dbkey).positive?
99
+
100
+ # Simulate TTL expiration by directly deleting the key but leaving instances entry
101
+ Familia.dbclient.del(dbkey)
102
+
103
+ # Now find_by_dbkey should return nil and clean up instances
104
+ result = RaceConditionUser.find_by_dbkey(dbkey)
105
+ exists_after = RaceConditionUser.instances.members.include?('race_user_1')
106
+ [exists_before, result, exists_after]
107
+ #=> [true, nil, false]
108
+
109
+ ## simulated race with check_exists: false also handles cleanup
110
+ user2 = RaceConditionUser.new(user_id: 'race_user_2', name: 'Race2', email: 'race2@example.com')
111
+ user2.save
112
+ dbkey2 = RaceConditionUser.dbkey('race_user_2')
113
+
114
+ # Delete key but leave instances entry
115
+ Familia.dbclient.del(dbkey2)
116
+
117
+ result = RaceConditionUser.find_by_dbkey(dbkey2, check_exists: false)
118
+ cleaned = !RaceConditionUser.instances.members.include?('race_user_2')
119
+ [result, cleaned]
120
+ #=> [nil, true]
121
+
122
+ # --- TTL Expiration Tests ---
123
+
124
+ ## TTL expiration leaves stale instances entry (demonstrating the problem)
125
+ session = RaceConditionSession.new(session_id: 'ttl_session_1', data: 'test data')
126
+ session.save
127
+ session.expire(1) # 1 second TTL
128
+
129
+ # Verify it's in instances
130
+ in_instances_before = RaceConditionSession.instances.members.include?('ttl_session_1')
131
+
132
+ # Wait for TTL to expire
133
+ sleep(1.5)
134
+
135
+ # Key is gone but instances entry remains (this is the stale entry problem)
136
+ key_exists = Familia.dbclient.exists(RaceConditionSession.dbkey('ttl_session_1')).positive?
137
+ in_instances_still = RaceConditionSession.instances.members.include?('ttl_session_1')
138
+ [in_instances_before, key_exists, in_instances_still]
139
+ #=> [true, false, true]
140
+
141
+ ## lazy cleanup fixes stale entry after TTL expiration
142
+ # Now when we try to load, it should clean up the stale entry
143
+ result = RaceConditionSession.find_by_dbkey(RaceConditionSession.dbkey('ttl_session_1'))
144
+ in_instances_after = RaceConditionSession.instances.members.include?('ttl_session_1')
145
+ [result, in_instances_after]
146
+ #=> [nil, false]
147
+
148
+ ## find methods clean up stale entries after TTL expiration
149
+ session2 = RaceConditionSession.new(session_id: 'ttl_session_2', data: 'test data 2')
150
+ session2.save
151
+ session2.expire(1)
152
+ sleep(1.5)
153
+
154
+ # Use find_by_id (which calls find_by_dbkey internally)
155
+ result = RaceConditionSession.find_by_id('ttl_session_2')
156
+ cleaned = !RaceConditionSession.instances.members.include?('ttl_session_2')
157
+ [result, cleaned]
158
+ #=> [nil, true]
159
+
160
+ # --- Count Consistency Tests ---
161
+
162
+ ## count reflects reality after lazy cleanup
163
+ RaceConditionUser.instances.clear
164
+ # Create real user
165
+ real = RaceConditionUser.new(user_id: 'count_real', name: 'Real', email: 'real@example.com')
166
+ real.save
167
+
168
+ # Add phantom entries
169
+ RaceConditionUser.instances.add('count_phantom_1', Familia.now)
170
+ RaceConditionUser.instances.add('count_phantom_2', Familia.now)
171
+
172
+ count_before = RaceConditionUser.count
173
+
174
+ # Trigger lazy cleanup by attempting to load phantoms
175
+ RaceConditionUser.find_by_id('count_phantom_1')
176
+ RaceConditionUser.find_by_id('count_phantom_2')
177
+
178
+ count_after = RaceConditionUser.count
179
+ real.destroy!
180
+ [count_before, count_after]
181
+ #=> [3, 1]
182
+
183
+ ## keys_count vs count after lazy cleanup
184
+ RaceConditionUser.instances.clear
185
+ real2 = RaceConditionUser.new(user_id: 'keys_count_real', name: 'Real', email: 'real@example.com')
186
+ real2.save
187
+ RaceConditionUser.instances.add('keys_count_phantom', Familia.now)
188
+
189
+ # Before cleanup: count includes phantom, keys_count doesn't
190
+ count_before = RaceConditionUser.count
191
+ keys_count_before = RaceConditionUser.keys_count
192
+
193
+ # Trigger lazy cleanup
194
+ RaceConditionUser.find_by_id('keys_count_phantom')
195
+
196
+ # After cleanup: both should match
197
+ count_after = RaceConditionUser.count
198
+ keys_count_after = RaceConditionUser.keys_count
199
+
200
+ real2.destroy!
201
+ [count_before, keys_count_before, count_after, keys_count_after]
202
+ #=> [2, 1, 1, 1]
203
+
204
+ # --- Edge Cases ---
205
+
206
+ ## empty identifier in key doesn't cause issues
207
+ # Key format with empty identifier would be "prefix::suffix"
208
+ # This shouldn't happen in practice, but we handle it gracefully
209
+ malformed_key = "#{RaceConditionUser.prefix}::object"
210
+ result = RaceConditionUser.find_by_dbkey(malformed_key)
211
+ result
212
+ #=> nil
213
+
214
+ ## key with unusual identifier characters
215
+ RaceConditionUser.instances.add('user:with:colons', Familia.now)
216
+ result = RaceConditionUser.find_by_dbkey(RaceConditionUser.dbkey('user:with:colons'))
217
+ # Should return nil (key doesn't exist) and attempt cleanup
218
+ # Note: cleanup may not work perfectly for identifiers with delimiters
219
+ result
220
+ #=> nil
221
+
222
+ ## concurrent load attempts on same stale entry
223
+ RaceConditionUser.instances.clear
224
+ RaceConditionUser.instances.add('concurrent_phantom', Familia.now)
225
+
226
+ threads = []
227
+ results = []
228
+ mutex = Mutex.new
229
+
230
+ 5.times do
231
+ threads << Thread.new do
232
+ r = RaceConditionUser.find_by_id('concurrent_phantom')
233
+ mutex.synchronize { results << r }
234
+ end
235
+ end
236
+
237
+ threads.each(&:join)
238
+
239
+ # All should return nil, and instances should be cleaned
240
+ all_nil = results.all?(&:nil?)
241
+ cleaned = !RaceConditionUser.instances.members.include?('concurrent_phantom')
242
+ [all_nil, cleaned, results.size]
243
+ #=> [true, true, 5]
244
+
245
+ # --- Cleanup ---
246
+
247
+ RaceConditionUser.instances.clear
248
+ RaceConditionSession.instances.clear
@@ -0,0 +1,318 @@
1
+ # try/features/relationships/class_level_multi_index_auto_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Tests for class-level multi_index auto-indexing and helper methods
6
+ #
7
+ # This file tests:
8
+ # 1. Auto-indexing on save - objects should automatically be added to class-level indexes
9
+ # 2. Index updates when field values change
10
+ # 3. Helper methods (update_all_indexes, remove_from_all_indexes, current_indexings)
11
+ #
12
+ # Auto-indexing for class-level indexes (within: :class) should work similarly
13
+ # to unique_index (within: nil), calling add_to_class_* methods on save.
14
+
15
+ require_relative '../../support/helpers/test_helpers'
16
+
17
+ # Test class for auto-indexing with class-level multi_index
18
+ class ::AutoIndexCustomer < Familia::Horreum
19
+ feature :relationships
20
+ include Familia::Features::Relationships::Indexing
21
+
22
+ identifier_field :custid
23
+ field :custid
24
+ field :name
25
+ field :status
26
+ field :tier
27
+
28
+ # Class-level multi_index (within: :class is default)
29
+ multi_index :status, :status_index
30
+
31
+ # Explicit within: :class for second index
32
+ multi_index :tier, :tier_index, within: :class
33
+
34
+ # Track instances for rebuild testing
35
+ class_sorted_set :instances, class: self, reference: true
36
+ end
37
+
38
+ # Clean up stale data from previous runs
39
+ %w[active pending inactive premium standard basic].each do |value|
40
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
41
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.tier_index_for(value).dbkey)
42
+ end
43
+ # Clean up any stale customer objects
44
+ %w[auto_001 auto_002 auto_003 auto_004 auto_nil auto_empty auto_ws].each do |custid|
45
+ existing = AutoIndexCustomer.find_by_identifier(custid, check_exists: false)
46
+ existing&.delete! if existing&.exists?
47
+ end
48
+
49
+ # =============================================
50
+ # 1. Auto-Indexing on Save Tests
51
+ # =============================================
52
+
53
+ ## New object saved - auto-indexed in status_index
54
+ @auto1 = AutoIndexCustomer.new(custid: 'auto_001', name: 'AutoAlice', status: 'active', tier: 'premium')
55
+ @auto1.save
56
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
57
+ #=> true
58
+
59
+ ## Auto-indexing works for tier_index too (explicit within: :class)
60
+ AutoIndexCustomer.tier_index_for('premium').members.include?('auto_001')
61
+ #=> true
62
+
63
+ ## Second object saved - joins same status index
64
+ @auto2 = AutoIndexCustomer.new(custid: 'auto_002', name: 'AutoBob', status: 'active', tier: 'standard')
65
+ @auto2.save
66
+ AutoIndexCustomer.status_index_for('active').members.sort
67
+ #=> ["auto_001", "auto_002"]
68
+
69
+ ## Different tier index is separate
70
+ AutoIndexCustomer.tier_index_for('standard').members
71
+ #=> ["auto_002"]
72
+
73
+ ## Object with different status creates new index entry
74
+ @auto3 = AutoIndexCustomer.new(custid: 'auto_003', name: 'AutoCharlie', status: 'pending', tier: 'basic')
75
+ @auto3.save
76
+ AutoIndexCustomer.status_index_for('pending').members
77
+ #=> ["auto_003"]
78
+
79
+ ## Active status index unchanged by new pending customer
80
+ AutoIndexCustomer.status_index_for('active').size
81
+ #=> 2
82
+
83
+ # =============================================
84
+ # 2. Query After Auto-Indexing
85
+ # =============================================
86
+
87
+ ## find_all_by_status returns auto-indexed customers
88
+ active_customers = AutoIndexCustomer.find_all_by_status('active')
89
+ active_customers.map(&:custid).sort
90
+ #=> ["auto_001", "auto_002"]
91
+
92
+ ## sample_from_status works with auto-indexed data
93
+ sample = AutoIndexCustomer.sample_from_status('active', 1)
94
+ ['auto_001', 'auto_002'].include?(sample.first&.custid)
95
+ #=> true
96
+
97
+ ## find_all_by_tier returns correct customers
98
+ premium_customers = AutoIndexCustomer.find_all_by_tier('premium')
99
+ premium_customers.map(&:custid)
100
+ #=> ["auto_001"]
101
+
102
+ # =============================================
103
+ # 3. Field Change and Index Update
104
+ # =============================================
105
+
106
+ ## Change status field and save - verify old index is NOT automatically updated
107
+ # Note: auto_update_class_indexes calls add_to_class_*, which is idempotent
108
+ # but does NOT remove from old index. Manual update is needed for field changes.
109
+ @old_status = @auto1.status
110
+ @auto1.status = 'inactive'
111
+ @auto1.save
112
+
113
+ # After save, customer is added to new index
114
+ AutoIndexCustomer.status_index_for('inactive').members.include?('auto_001')
115
+ #=> true
116
+
117
+ ## Old index still contains the entry (save doesn't auto-remove from old)
118
+ # This is expected behavior - save is idempotent add, not update
119
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
120
+ #=> true
121
+
122
+ ## Use update_in_class_status_index to properly move between indexes
123
+ @auto1.update_in_class_status_index(@old_status)
124
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
125
+ #=> false
126
+
127
+ ## Customer now only in inactive index
128
+ AutoIndexCustomer.status_index_for('inactive').members
129
+ #=> ["auto_001"]
130
+
131
+ # =============================================
132
+ # 4. Helper Methods - update_all_indexes
133
+ # =============================================
134
+
135
+ ## update_all_indexes method exists
136
+ @auto2.respond_to?(:update_all_indexes)
137
+ #=> true
138
+
139
+ ## update_all_indexes updates both status and tier indexes
140
+ @old_values = { status: @auto2.status, tier: @auto2.tier }
141
+ @auto2.status = 'pending'
142
+ @auto2.tier = 'premium'
143
+ @auto2.update_all_indexes(@old_values)
144
+
145
+ # Check status index updated
146
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_002')
147
+ #=> false
148
+
149
+ ## New status index contains customer
150
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_002')
151
+ #=> true
152
+
153
+ ## Old tier index no longer contains customer
154
+ AutoIndexCustomer.tier_index_for('standard').members.include?('auto_002')
155
+ #=> false
156
+
157
+ ## New tier index contains customer
158
+ AutoIndexCustomer.tier_index_for('premium').members.include?('auto_002')
159
+ #=> true
160
+
161
+ # =============================================
162
+ # 5. Helper Methods - remove_from_all_indexes
163
+ # =============================================
164
+
165
+ ## remove_from_all_indexes method exists
166
+ @auto3.respond_to?(:remove_from_all_indexes)
167
+ #=> true
168
+
169
+ ## Verify customer is in indexes before removal
170
+ [
171
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_003'),
172
+ AutoIndexCustomer.tier_index_for('basic').members.include?('auto_003')
173
+ ]
174
+ #=> [true, true]
175
+
176
+ ## remove_from_all_indexes removes from all class-level indexes
177
+ @auto3.remove_from_all_indexes
178
+ [
179
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_003'),
180
+ AutoIndexCustomer.tier_index_for('basic').members.include?('auto_003')
181
+ ]
182
+ #=> [false, false]
183
+
184
+ # =============================================
185
+ # 6. Helper Methods - current_indexings
186
+ # =============================================
187
+
188
+ ## current_indexings method exists
189
+ @auto1.respond_to?(:current_indexings)
190
+ #=> true
191
+
192
+ ## Re-add auto1 to indexes for testing current_indexings
193
+ @auto1.add_to_class_status_index
194
+ @auto1.add_to_class_tier_index
195
+ @indexings = @auto1.current_indexings
196
+ @indexings.length
197
+ #=> 2
198
+
199
+ ## current_indexings returns correct info for status_index
200
+ @status_indexing = @indexings.find { |i| i[:index_name] == :status_index }
201
+ [@status_indexing[:field], @status_indexing[:cardinality], @status_indexing[:type]]
202
+ #=> [:status, :multi, "multi_index"]
203
+
204
+ ## current_indexings returns correct info for tier_index
205
+ @tier_indexing = @indexings.find { |i| i[:index_name] == :tier_index }
206
+ [@tier_indexing[:field], @tier_indexing[:cardinality], @tier_indexing[:type]]
207
+ #=> [:tier, :multi, "multi_index"]
208
+
209
+ ## current_indexings shows scope_class for class-level indexes
210
+ @status_indexing[:scope_class]
211
+ #=> "class"
212
+
213
+ # =============================================
214
+ # 7. Helper Methods - indexed_in?
215
+ # =============================================
216
+
217
+ ## indexed_in? method exists
218
+ @auto1.respond_to?(:indexed_in?)
219
+ #=> true
220
+
221
+ ## indexed_in? returns true for indexes where customer is present
222
+ @auto1.indexed_in?(:status_index)
223
+ #=> true
224
+
225
+ ## indexed_in? returns true for tier_index too
226
+ @auto1.indexed_in?(:tier_index)
227
+ #=> true
228
+
229
+ ## indexed_in? returns false for non-existent index
230
+ @auto1.indexed_in?(:nonexistent_index)
231
+ #=> false
232
+
233
+ ## After removal, indexed_in? returns false
234
+ @auto1.remove_from_class_status_index
235
+ @auto1.indexed_in?(:status_index)
236
+ #=> false
237
+
238
+ # =============================================
239
+ # 8. Delete and Index Cleanup
240
+ # =============================================
241
+
242
+ ## Create a new customer for delete testing
243
+ @auto4 = AutoIndexCustomer.new(custid: 'auto_004', name: 'AutoDiana', status: 'active', tier: 'standard')
244
+ @auto4.save
245
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_004')
246
+ #=> true
247
+
248
+ ## Delete does NOT automatically remove from indexes (by design)
249
+ # Applications should call remove_from_all_indexes before delete if needed
250
+ @auto4.delete!
251
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_004')
252
+ #=> true
253
+
254
+ ## Manual cleanup required after delete
255
+ # Note: This tests that stale entries exist until explicitly cleaned
256
+ # The find_by methods handle stale entries gracefully
257
+ stale_customer = AutoIndexCustomer.find_by_identifier('auto_004')
258
+ stale_customer
259
+ #=> nil
260
+
261
+ # =============================================
262
+ # 9. Edge Cases
263
+ # =============================================
264
+
265
+ ## Nil field value - should not be indexed
266
+ @auto_nil = AutoIndexCustomer.new(custid: 'auto_nil', name: 'NilStatus', status: nil, tier: 'basic')
267
+ @auto_nil.save
268
+ # Should not create an entry in the nil index (empty string key)
269
+ AutoIndexCustomer.status_index_for('').members.include?('auto_nil')
270
+ #=> false
271
+
272
+ ## Empty string field value - should not be indexed
273
+ @auto_empty = AutoIndexCustomer.new(custid: 'auto_empty', name: 'EmptyStatus', status: '', tier: 'basic')
274
+ @auto_empty.save
275
+ AutoIndexCustomer.status_index_for('').members.include?('auto_empty')
276
+ #=> false
277
+
278
+ ## Whitespace-only field value - should not be indexed
279
+ @auto_ws = AutoIndexCustomer.new(custid: 'auto_ws', name: 'WSStatus', status: ' ', tier: 'basic')
280
+ @auto_ws.save
281
+ AutoIndexCustomer.status_index_for(' ').members.include?('auto_ws')
282
+ #=> false
283
+
284
+ # =============================================
285
+ # 10. Idempotent Save Behavior
286
+ # =============================================
287
+
288
+ ## Saving same object multiple times doesn't duplicate index entries
289
+ @auto2.save
290
+ @auto2.save
291
+ @auto2.save
292
+ # Should still only have one entry
293
+ AutoIndexCustomer.status_index_for(@auto2.status).members.count { |m| m == 'auto_002' }
294
+ #=> 1
295
+
296
+ ## UnsortedSet inherently prevents duplicates
297
+ AutoIndexCustomer.status_index_for('pending').size
298
+ #=> 1
299
+
300
+ # Teardown
301
+ # Clean up test objects
302
+ [@auto1, @auto2, @auto3, @auto_nil, @auto_empty, @auto_ws].compact.each do |obj|
303
+ obj.delete! if obj.respond_to?(:exists?) && obj.exists?
304
+ end
305
+
306
+ # Clean up index keys
307
+ %w[active pending inactive premium standard basic].each do |value|
308
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
309
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.tier_index_for(value).dbkey)
310
+ end
311
+
312
+ # Clean up edge case indexes
313
+ ['', ' '].each do |value|
314
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
315
+ end
316
+
317
+ # Clean up instances collection
318
+ AutoIndexCustomer.instances.clear if AutoIndexCustomer.respond_to?(:instances)