familia 2.7.0 → 2.9.0
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/.pre-commit-config.yaml +0 -2
- data/CHANGELOG.rst +236 -0
- data/Gemfile.lock +2 -2
- data/docs/guides/feature-housekeeping.md +66 -36
- data/docs/migrating/v2.9.0.md +125 -0
- data/familia.gemspec +1 -1
- data/lib/familia/batch_result.rb +158 -0
- data/lib/familia/connection/handlers.rb +131 -47
- data/lib/familia/connection/operations.rb +5 -4
- data/lib/familia/connection/pipelined_core.rb +14 -7
- data/lib/familia/connection/transaction_core.rb +9 -0
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/collection_base.rb +132 -0
- data/lib/familia/data_type/connection.rb +21 -52
- data/lib/familia/data_type/scalar_base.rb +33 -0
- data/lib/familia/data_type/types/hashkey.rb +284 -0
- data/lib/familia/data_type/types/json_stringkey.rb +2 -0
- data/lib/familia/data_type/types/listkey.rb +151 -17
- data/lib/familia/data_type/types/sorted_set.rb +455 -21
- data/lib/familia/data_type/types/stringkey.rb +166 -0
- data/lib/familia/data_type/types/unsorted_set.rb +155 -15
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/errors.rb +4 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +18 -0
- data/lib/familia/features/housekeeping.rb +112 -18
- data/lib/familia/field_type.rb +18 -0
- data/lib/familia/horreum/connection.rb +15 -10
- data/lib/familia/horreum/definition.rb +18 -0
- data/lib/familia/horreum/management/audit.rb +37 -39
- data/lib/familia/horreum/persistence.rb +23 -19
- data/lib/familia/horreum.rb +9 -6
- data/lib/familia/multi_result.rb +111 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -1
- data/try/edge_cases/fast_writer_transaction_guard_try.rb +130 -0
- data/try/edge_cases/iterator_connection_errors_try.rb +97 -0
- data/try/edge_cases/pipeline_handler_edge_cases_try.rb +214 -0
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/atomic_write_coverage_try.rb +1 -1
- data/try/features/atomic_write_try.rb +11 -9
- data/try/features/atomicity_try.rb +2 -2
- data/try/features/dirty_tracking_try.rb +21 -21
- data/try/features/housekeeping/housekeeping_try.rb +200 -21
- data/try/features/instance_registry_try.rb +2 -2
- data/try/integration/connection/handler_constraints_try.rb +8 -8
- data/try/integration/connection/operation_mode_guards_try.rb +3 -3
- data/try/integration/connection/pipeline_fallback_integration_try.rb +4 -4
- data/try/integration/connection/pipeline_handler_integration_try.rb +175 -0
- data/try/integration/connection/pipeline_horreum_routing_try.rb +147 -0
- data/try/integration/connection/pools_try.rb +1 -1
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -4
- data/try/integration/connection/transaction_mode_permissive_try.rb +8 -8
- data/try/integration/connection/transaction_mode_strict_try.rb +2 -2
- data/try/integration/connection/transaction_mode_warn_try.rb +5 -5
- data/try/integration/connection/transaction_modes_try.rb +14 -14
- data/try/integration/data_types/datatype_pipelines_try.rb +35 -15
- data/try/integration/data_types/datatype_transactions_try.rb +30 -28
- data/try/integration/database_consistency_try.rb +1 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/transaction_safety_core_try.rb +1 -1
- data/try/integration/transaction_safety_workflow_try.rb +2 -2
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +1 -1
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +1 -1
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +1 -1
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +11 -19
- data/try/unit/batch_result_try.rb +348 -0
- data/try/unit/data_types/each_record_try.rb +298 -0
- data/try/unit/data_types/enumerable_consistency/concurrent_modification_try.rb +176 -0
- data/try/unit/data_types/enumerable_consistency/hashkey_consistency_try.rb +224 -0
- data/try/unit/data_types/enumerable_consistency/large_scale_consistency_try.rb +292 -0
- data/try/unit/data_types/enumerable_consistency/listkey_consistency_try.rb +230 -0
- data/try/unit/data_types/enumerable_consistency/sorted_set_consistency_try.rb +241 -0
- data/try/unit/data_types/enumerable_consistency/unsorted_set_consistency_try.rb +261 -0
- data/try/unit/data_types/enumerable_try.rb +228 -0
- data/try/unit/data_types/hashkey_each_try.rb +213 -0
- data/try/unit/data_types/hashkey_operations_try.rb +269 -0
- data/try/unit/data_types/list_commands_try.rb +314 -0
- data/try/unit/data_types/listkey_each_try.rb +222 -0
- data/try/unit/data_types/sorted_set_each_try.rb +227 -0
- data/try/unit/data_types/sortedset_operations_try.rb +467 -0
- data/try/unit/data_types/stringkey_extended_try.rb +239 -0
- data/try/unit/data_types/unsorted_set_each_try.rb +185 -0
- data/try/unit/data_types/unsortedset_operations_try.rb +174 -0
- data/try/unit/fiber_pipeline_handler_try.rb +147 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/json_type_preservation_try.rb +3 -3
- data/try/unit/horreum/multi_field_update_try.rb +143 -0
- data/try/unit/horreum/serialization_try.rb +14 -14
- metadata +33 -5
- data/changelog.d/20260514_034522_claude_review_familia_issue_217.rst +0 -46
- data/lib/multi_result.rb +0 -109
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42bbbbcb737ab4222505955e5b1af2ab72ed962ba80a02cf324206b4f2379b94
|
|
4
|
+
data.tar.gz: 331b1d0bb0808618a87d962e1b09af61a31d59b7b8d56b0f49513cb9f3fec63d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ff40710ce23c2f3dadbfbf68142800b8f10590c898f30f424819a4f0efea9aa545c52307c487c6fd96bf0c0f95f7504e5614056a30039f75f9db84fe1fcd595
|
|
7
|
+
data.tar.gz: be6c618b3fab3eb5ac8577c8a4bd7fbbbc53ce300da750baf3f64d70807a0aeb2a3a7a7f28da91d7637ebe86d9f2ccc86b677478ed90ab3ae7878d6d8816cd09
|
data/.pre-commit-config.yaml
CHANGED
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,242 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.9.0:
|
|
11
|
+
|
|
12
|
+
2.9.0 — 2026-05-17
|
|
13
|
+
==================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Batch iteration primitives for DataTypes via ``Enumerable`` integration:
|
|
19
|
+
|
|
20
|
+
- All DataTypes (``SortedSet``, ``HashKey``, ``UnsortedSet``, ``ListKey``) now
|
|
21
|
+
``include Enumerable``, providing ``each_slice``, ``lazy``, ``map``, ``reduce``,
|
|
22
|
+
``find``, and other stdlib methods.
|
|
23
|
+
|
|
24
|
+
- **SortedSet#each(since:, until:)**: Cursor-based iteration with optional
|
|
25
|
+
timestamp bounds. Uses ZRANGEBYSCORE when bounds provided (inclusive),
|
|
26
|
+
ZSCAN otherwise. Accepts Time objects or numeric scores.
|
|
27
|
+
|
|
28
|
+
- **HashKey#each(matching:)**: Cursor-based iteration via HSCAN with optional
|
|
29
|
+
glob pattern filter on field names.
|
|
30
|
+
|
|
31
|
+
- **UnsortedSet#each(matching:)**: Cursor-based iteration via SSCAN with optional
|
|
32
|
+
glob pattern filter using Redis SSCAN MATCH on raw values.
|
|
33
|
+
|
|
34
|
+
- **ListKey#each(batch_size:)**: Memory-efficient LRANGE pagination for large lists.
|
|
35
|
+
|
|
36
|
+
- ``DataType#each_record(batch_size:, write_size:, **filters)`` yields loaded
|
|
37
|
+
Horreum records (not raw IDs) via ``load_multi``. Ghost instances (expired keys
|
|
38
|
+
still in ``instances``) are automatically filtered. The ``write_size:`` parameter
|
|
39
|
+
controls pipelining depth (``nil`` for serial execution).
|
|
40
|
+
|
|
41
|
+
- ``Familia::BatchResult`` value type for aggregating batch operation results:
|
|
42
|
+
|
|
43
|
+
- ``BatchResult.collect(enumerable, strict: false) { |record| ... }`` iterates
|
|
44
|
+
any Enumerable, tracking ``scanned``, ``modified`` (truthy returns), ``errors``
|
|
45
|
+
(array of ``{id:, error:}``), and ``duration_ms``.
|
|
46
|
+
|
|
47
|
+
- Per-record exception isolation: errors are captured and iteration continues.
|
|
48
|
+
|
|
49
|
+
- ``strict: true`` re-raises collected errors after iteration completes.
|
|
50
|
+
|
|
51
|
+
Changed
|
|
52
|
+
-------
|
|
53
|
+
|
|
54
|
+
- Renamed batch field-update methods for clarity:
|
|
55
|
+
|
|
56
|
+
- ``batch_update`` is now ``multi_field_update``
|
|
57
|
+
- ``batch_fast_write`` is now ``multi_field_fast_write``
|
|
58
|
+
|
|
59
|
+
Old names removed without deprecation shim (breaking change).
|
|
60
|
+
|
|
61
|
+
- Moved ``MultiResult`` into Familia namespace as ``Familia::MultiResult``.
|
|
62
|
+
Old top-level constant removed without backwards-compat alias (breaking change).
|
|
63
|
+
|
|
64
|
+
AI Assistance
|
|
65
|
+
-------------
|
|
66
|
+
|
|
67
|
+
- Implementation and test coverage developed with parallel Claude Code agents:
|
|
68
|
+
one for production code (DataType iteration, BatchResult, renames), one for
|
|
69
|
+
Tryouts test suite (228 new tests across 8 files). PR #264.
|
|
70
|
+
|
|
71
|
+
.. _changelog-2.8.0:
|
|
72
|
+
|
|
73
|
+
2.8.0 — 2026-05-15
|
|
74
|
+
==================
|
|
75
|
+
|
|
76
|
+
Added
|
|
77
|
+
-----
|
|
78
|
+
|
|
79
|
+
- Expanded Redis 7 command coverage across all DataType classes:
|
|
80
|
+
|
|
81
|
+
- **StringKey**: ``incrbyfloat``, ``getex``, ``getdel``, ``setex``, ``psetex``,
|
|
82
|
+
``bitcount``, ``bitpos``, ``bitfield``, plus class methods ``mget``, ``mset``,
|
|
83
|
+
``msetnx``, ``bitop`` for multi-key and bitwise operations.
|
|
84
|
+
|
|
85
|
+
- **List**: ``trim``/``ltrim``, ``set``/``lset``, ``insert``/``linsert``,
|
|
86
|
+
``move``/``lmove``, ``pushx``/``rpushx``, ``unshiftx``/``lpushx``.
|
|
87
|
+
Updated ``pop`` and ``shift`` to support optional count parameter for batch operations.
|
|
88
|
+
|
|
89
|
+
- **UnsortedSet**: ``intersection``/``inter``, ``union``, ``difference``/``diff``,
|
|
90
|
+
``member_any?``/``members?``, ``scan``, ``intercard``/``intersection_cardinality``,
|
|
91
|
+
``interstore``/``intersection_store``, ``unionstore``/``union_store``,
|
|
92
|
+
``diffstore``/``difference_store``.
|
|
93
|
+
|
|
94
|
+
- **SortedSet**: ``popmin``, ``popmax``, ``score_count``/``zcount``, ``mscore``,
|
|
95
|
+
``union``, ``inter``, ``rangebylex``, ``revrangebylex``, ``remrangebylex``,
|
|
96
|
+
``lexcount``, ``randmember``, ``scan``, ``unionstore``, ``interstore``,
|
|
97
|
+
``diff``, ``diffstore``.
|
|
98
|
+
|
|
99
|
+
- **HashKey**: ``scan``/``hscan``, ``incrbyfloat``/``incrfloat``,
|
|
100
|
+
``strlen``/``hstrlen``, ``randfield``/``hrandfield``, plus field-level TTL
|
|
101
|
+
commands (Redis 7.4+): ``expire_fields``, ``pexpire_fields``, ``expireat_fields``,
|
|
102
|
+
``pexpireat_fields``, ``ttl_fields``, ``pttl_fields``, ``persist_fields``,
|
|
103
|
+
``expiretime_fields``, ``pexpiretime_fields``.
|
|
104
|
+
|
|
105
|
+
- Added 158 new tests across 5 test files covering all new methods.
|
|
106
|
+
|
|
107
|
+
- Instance-scoped ``audit_multi_indexes`` is now fully implemented.
|
|
108
|
+
Discovers per-scope bucket keys via SCAN, partitions them by scope
|
|
109
|
+
instance, and reports stale members, orphaned buckets, and missing
|
|
110
|
+
entries in the same shape as the class-level audit. Orphan entries
|
|
111
|
+
carry a ``:reason`` (``:scope_missing`` or ``:field_value_unheld``)
|
|
112
|
+
and a ``:scope_id``. Missing entries are detected via the indexed
|
|
113
|
+
class's ``participates_in`` relationship to the scope class; when
|
|
114
|
+
absent, the result carries ``missing_status: :not_audited``.
|
|
115
|
+
Resolves the ``:not_implemented`` follow-up from #217.
|
|
116
|
+
|
|
117
|
+
- ``repair_multi_indexes!`` class method that invokes the existing
|
|
118
|
+
``rebuild_<index_name>`` methods for both class-level (one call on
|
|
119
|
+
the indexed class) and instance-scoped (one call per scope
|
|
120
|
+
instance) multi-indexes. Indexes whose audit status is ``:ok`` are
|
|
121
|
+
skipped; rebuild methods that don't exist or scope classes
|
|
122
|
+
without an ``instances`` collection are recorded in ``:skipped``
|
|
123
|
+
with a reason.
|
|
124
|
+
|
|
125
|
+
- ``housekeeping`` feature gains a class-level bulk runner,
|
|
126
|
+
``Klass.run_chores!(chore_name:, limit:, batch_size:)``. It iterates
|
|
127
|
+
the class's ``instances`` collection in pipelined batches via
|
|
128
|
+
``load_multi``, runs all registered chores (or one named chore)
|
|
129
|
+
against each record, and returns a stats hash:
|
|
130
|
+
``{ model:, scanned:, chores: { name => { modified:, errors: } } }``.
|
|
131
|
+
Truthy chore returns increment ``modified``; raised exceptions are
|
|
132
|
+
isolated per-record, logged via ``Familia.warn``, and counted as
|
|
133
|
+
``errors`` so a single failure doesn't halt the run. Lifted from the
|
|
134
|
+
shape proven out in OneTime Secret's ``HousekeepingJob``.
|
|
135
|
+
|
|
136
|
+
- Trace events for connection-mode conflicts. ``Familia.trace`` now
|
|
137
|
+
emits ``CONFLICTING_CONTEXT`` when pipeline and transaction
|
|
138
|
+
contexts collide (in both ``FiberPipelineHandler``/
|
|
139
|
+
``FiberTransactionHandler`` and the
|
|
140
|
+
``execute_transaction``/``execute_pipeline`` entry points), and
|
|
141
|
+
``FAST_WRITER_BLOCKED`` when a fast writer (``field!``) is called
|
|
142
|
+
inside a transaction or pipeline. These fire just before the
|
|
143
|
+
corresponding ``ConflictingContextError`` /
|
|
144
|
+
``OperationModeError`` is raised, so operators can pinpoint where
|
|
145
|
+
blocked operations originate when ``FAMILIA_TRACE=1``.
|
|
146
|
+
|
|
147
|
+
Changed
|
|
148
|
+
-------
|
|
149
|
+
|
|
150
|
+
- ``repair_all!`` now runs each repair stage inside its own rescue
|
|
151
|
+
boundary; a failure in one dimension no longer prevents the others
|
|
152
|
+
from running. The return hash gains ``:status`` (``:ok`` or
|
|
153
|
+
``:partial_failure``), ``:errors`` (per-stage exception details
|
|
154
|
+
when raised), and ``:multi_indexes`` (results from the new
|
|
155
|
+
``repair_multi_indexes!``). An opt-in ``verify: true`` kwarg
|
|
156
|
+
re-runs ``health_check`` after repair and exposes the result as
|
|
157
|
+
``:post_audit`` / ``:verified`` so callers can confirm the run
|
|
158
|
+
actually drove the model back to a healthy state.
|
|
159
|
+
|
|
160
|
+
- ``AuditReport#complete?`` is no longer false-positive due to
|
|
161
|
+
``:not_implemented`` stubs in ``multi_indexes`` -- instance-scoped
|
|
162
|
+
indexes return ``:ok`` or ``:issues_found`` like class-level ones.
|
|
163
|
+
|
|
164
|
+
- ``housekeeping`` feature: split the dual-purpose ``tidy!`` into two
|
|
165
|
+
explicit instance methods. ``do_chore!(name)`` runs a single named
|
|
166
|
+
chore and returns the block's raw return value (no longer wrapped
|
|
167
|
+
in a ``{name => result}`` hash). ``do_chores!`` runs every
|
|
168
|
+
registered chore and returns the ``{name => result}`` hash.
|
|
169
|
+
``tidy!`` is preserved as an alias of ``do_chores!`` for backwards
|
|
170
|
+
compatibility with the 2.7.0 no-arg call site; the single-arg form
|
|
171
|
+
``tidy!(:name)`` now raises ``ArgumentError``.
|
|
172
|
+
|
|
173
|
+
- The connection handler hierarchy has been refactored from class
|
|
174
|
+
inheritance (``BaseConnectionHandler``) to module composition.
|
|
175
|
+
Handlers now ``include Familia::Connection::Handler`` and declare
|
|
176
|
+
their operation-mode capabilities with a small DSL:
|
|
177
|
+
``supports transaction: true, pipelined: false``. The
|
|
178
|
+
``BaseConnectionHandler`` constant is gone. This is only relevant if
|
|
179
|
+
you have custom handlers in application code — the public
|
|
180
|
+
``allows_transaction`` / ``allows_pipelined`` class methods continue
|
|
181
|
+
to work, and the singleton ``.instance`` accessors on
|
|
182
|
+
``FiberPipelineHandler`` / ``FiberTransactionHandler`` are
|
|
183
|
+
unchanged. The previous default of "allow all operations" when
|
|
184
|
+
capability flags were not set has been removed; every handler is now
|
|
185
|
+
expected to declare its capabilities explicitly via ``supports``.
|
|
186
|
+
- ``Familia.dbclient`` and ``Familia::DataType#dbclient`` now route through ``FiberPipelineHandler`` before ``FiberTransactionHandler``, matching ``Horreum#dbclient``. With both handlers in the chain, attempting to mix pipeline and transaction contexts raises ``Familia::ConflictingContextError`` uniformly from every call site.
|
|
187
|
+
|
|
188
|
+
Removed
|
|
189
|
+
-------
|
|
190
|
+
|
|
191
|
+
- ``Familia::DataType#direct_access`` has been removed. The method
|
|
192
|
+
was a legacy escape hatch for issuing raw Redis commands from
|
|
193
|
+
inside a DataType wrapper; it predates the chain-based routing of
|
|
194
|
+
``Fiber[:familia_transaction]`` and ``Fiber[:familia_pipeline]``.
|
|
195
|
+
All in-tree call sites now go through the wrapper's own mutating
|
|
196
|
+
methods (which auto-route through the active transaction or
|
|
197
|
+
pipeline) or through the wrapper's ``transaction`` / ``pipelined``
|
|
198
|
+
blocks. If you were calling ``direct_access do |conn, key| ... end``,
|
|
199
|
+
replace it with either the DataType's own mutator or the
|
|
200
|
+
corresponding block API.
|
|
201
|
+
|
|
202
|
+
Fixed
|
|
203
|
+
-----
|
|
204
|
+
|
|
205
|
+
- ``SortedSet#popmin`` and ``SortedSet#popmax`` now normalize an explicitly
|
|
206
|
+
passed ``nil`` count to the default of ``1``. Previously, calling
|
|
207
|
+
``zset.popmin(nil)`` or ``zset.popmax(nil)`` would bypass the ``count == 1``
|
|
208
|
+
branch of the structural dispatch added in the prior commit, causing
|
|
209
|
+
redis-rb's flat ``[member, score]`` return shape to be iterated as if it
|
|
210
|
+
were a nested result — yielding a malformed pair. Omitting the argument
|
|
211
|
+
was and remains unaffected.
|
|
212
|
+
|
|
213
|
+
- Restored ``require 'set'`` in ``lib/familia/horreum/management/audit.rb``. ``Set`` is autoloaded as a core class only on Ruby 3.4+; on Ruby 3.2/3.3 (the gem's supported floor) the require is mandatory for the five ``Set.new`` usages in that file.
|
|
214
|
+
|
|
215
|
+
AI Assistance
|
|
216
|
+
-------------
|
|
217
|
+
|
|
218
|
+
- Claude Opus 4.5 analyzed Redis 7 command documentation and compared coverage
|
|
219
|
+
against existing Familia DataType implementations using parallel Explore agents.
|
|
220
|
+
- Implementation performed by 5 parallel backend-dev agents, one per DataType.
|
|
221
|
+
- Test coverage written by 5 parallel qa-automation-engineer agents focusing on
|
|
222
|
+
Familia-specific behavior (serialization, deserialization, aliases) rather than
|
|
223
|
+
re-testing redis-rb gem functionality.
|
|
224
|
+
|
|
225
|
+
- Edge case identified by the Claude Code Review GitHub Action
|
|
226
|
+
(``.github/workflows/claude-code-review.yml``) when reviewing the
|
|
227
|
+
structural-dispatch change in commit ``010d5be``. Fix drafted and verified
|
|
228
|
+
by Claude Opus 4.7 under supervision.
|
|
229
|
+
|
|
230
|
+
- Instance-scoped multi-index audit algorithm (bucket discovery,
|
|
231
|
+
scope existence batching, participation-driven missing detection),
|
|
232
|
+
``repair_multi_indexes!``, the ``repair_all!`` robustness
|
|
233
|
+
refactor, and the accompanying tryouts coverage were authored
|
|
234
|
+
with Claude Code assistance against the #217 review branch.
|
|
235
|
+
|
|
236
|
+
- Method split, alias wiring, bulk runner port from OTS, doc updates,
|
|
237
|
+
and expanded tryouts coverage (25 → 48 testcases) authored with
|
|
238
|
+
Claude Code.
|
|
239
|
+
|
|
240
|
+
- Added the trace instrumentation in response to PR #263 review
|
|
241
|
+
feedback (Claude Code review bot) recommending tracing for
|
|
242
|
+
conflict detection events.
|
|
243
|
+
|
|
244
|
+
- The handler refactor, ``direct_access`` removal, and changelog drafting were performed with Claude Code assistance while resolving review feedback on PR #263.
|
|
245
|
+
|
|
10
246
|
.. _changelog-2.7.0:
|
|
11
247
|
|
|
12
248
|
2.7.0 — 2026-05-13
|
data/Gemfile.lock
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.
|
|
4
|
+
familia (2.9.0)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
6
|
connection_pool (>= 2.4, < 4.0)
|
|
7
7
|
csv (~> 3.3)
|
|
8
8
|
json_schemer (~> 2.0)
|
|
9
9
|
logger (~> 1.7)
|
|
10
10
|
oj (~> 3.16)
|
|
11
|
-
redis (>=
|
|
11
|
+
redis (>= 5.0, < 6.0)
|
|
12
12
|
stringio (~> 3.1.1)
|
|
13
13
|
uri-valkey (~> 1.4)
|
|
14
14
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The Housekeeping feature provides a declarative DSL for registering named cleanup chores on Horreum models. It is designed for short-lived, repeated tidying against fields whose values have drifted over time -- not for versioned, one-shot migrations.
|
|
4
4
|
|
|
5
5
|
> [!TIP]
|
|
6
|
-
> Enable with `feature :housekeeping` and register cleanup blocks with `chore :name do |obj| ... end`. Run them with `obj.tidy
|
|
6
|
+
> Enable with `feature :housekeeping` and register cleanup blocks with `chore :name do |obj| ... end`. Run all of them with `obj.do_chores!` (aliased `tidy!`), or one with `obj.do_chore!(:name)`. Iteration and persistence are the caller's responsibility.
|
|
7
7
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
|
|
@@ -27,8 +27,12 @@ class Organization < Familia::Horreum
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
org = Organization.from_identifier("acme-corp")
|
|
30
|
-
org.
|
|
30
|
+
org.do_chores!
|
|
31
31
|
# => { standardize_planid: true }
|
|
32
|
+
|
|
33
|
+
# Or run a single chore by name (returns the block's raw value):
|
|
34
|
+
org.do_chore!(:standardize_planid)
|
|
35
|
+
# => true
|
|
32
36
|
```
|
|
33
37
|
|
|
34
38
|
## When to Use
|
|
@@ -79,35 +83,43 @@ Run all registered chores, or one by name:
|
|
|
79
83
|
```ruby
|
|
80
84
|
user = User.from_identifier("alice@example.com")
|
|
81
85
|
|
|
82
|
-
user.
|
|
86
|
+
user.do_chores!
|
|
83
87
|
# => { downcase_email: true, default_timezone: nil }
|
|
84
88
|
|
|
85
|
-
user.tidy!
|
|
86
|
-
# => { downcase_email:
|
|
89
|
+
user.tidy! # alias for do_chores!
|
|
90
|
+
# => { downcase_email: nil, default_timezone: nil }
|
|
91
|
+
|
|
92
|
+
user.do_chore!(:downcase_email)
|
|
93
|
+
# => true
|
|
87
94
|
```
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
`do_chores!` returns a hash mapping chore name to the block's return value. `do_chore!` returns the block's raw return value (not wrapped in a hash). A truthy result signals "modified"; `nil` or `false` signals "no-op". The feature does not interpret these values -- they are passed through for the caller's stats collection.
|
|
90
97
|
|
|
91
|
-
### Iteration --
|
|
98
|
+
### Iteration -- Bulk Runner
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
For running chores across every record, the feature ships a class-level `run_chores!` that iterates the `instances` collection in pipelined batches (via `load_multi`), executes each chore per record with error isolation, and returns a stats hash:
|
|
94
101
|
|
|
95
102
|
```ruby
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
Organization.run_chores!
|
|
104
|
+
# => {
|
|
105
|
+
# model: "Organization",
|
|
106
|
+
# scanned: 4200,
|
|
107
|
+
# chores: {
|
|
108
|
+
# standardize_planid: { modified: 37, errors: 0 },
|
|
109
|
+
# uppercase_country: { modified: 102, errors: 1 },
|
|
110
|
+
# },
|
|
111
|
+
# }
|
|
112
|
+
|
|
113
|
+
Organization.run_chores!(chore_name: :standardize_planid, limit: 500)
|
|
114
|
+
# Filter to one chore and cap records scanned.
|
|
115
|
+
|
|
116
|
+
Organization.run_chores!(batch_size: 50)
|
|
117
|
+
# Tune the load_multi pipeline batch size (default: 100).
|
|
108
118
|
```
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
A truthy chore return increments `modified`; a raised exception increments `errors` (logged via `Familia.warn`) and iteration continues. The runner requires the class to expose `instances` (Horreum's default class-level sorted set) and `load_multi`.
|
|
121
|
+
|
|
122
|
+
For scheduling, cross-model orchestration, custom logging, or non-default iteration (e.g. a configured allowlist of model classes), wrap `run_chores!` in your own job. The feature deliberately stays out of cron, multi-model discovery, and project-specific logging layers.
|
|
111
123
|
|
|
112
124
|
## Generated Method Reference
|
|
113
125
|
|
|
@@ -117,15 +129,18 @@ The feature has no opinion about batching, SCAN vs KEYS, error aggregation, or s
|
|
|
117
129
|
|-------|--------|---------|
|
|
118
130
|
| **Class** | `chore(name, &block)` | Register a chore |
|
|
119
131
|
| | `chores` | Hash of registered chores |
|
|
120
|
-
|
|
|
132
|
+
| | `run_chores!(chore_name:, limit:, batch_size:)` | Bulk runner across `instances`; returns stats hash |
|
|
133
|
+
| **Instance** | `do_chore!(name)` | Run a single chore by name; returns the block's raw value |
|
|
134
|
+
| | `do_chores!` | Run every registered chore; returns Hash |
|
|
135
|
+
| | `tidy!` | Alias for `do_chores!` |
|
|
121
136
|
|
|
122
137
|
## Design Constraints
|
|
123
138
|
|
|
124
139
|
1. **No implicit saves.** The block must call `save` (or `commit_fields`) itself. The feature does not auto-persist.
|
|
125
|
-
2. **
|
|
140
|
+
2. **Bulk via `run_chores!` only.** The feature operates on a single instance (`do_chore!`/`do_chores!`) plus one bulk runner (`run_chores!`) that iterates `instances`. Scheduling, multi-model orchestration, and custom logging stay in the consumer app.
|
|
126
141
|
3. **No ordering.** Chores run in registration order, but should not depend on each other. If order matters, write one chore with sequential steps.
|
|
127
142
|
4. **Idempotent by convention.** Use the conditional pattern (`if canonical && canonical != org.planid`) so a second run is a no-op.
|
|
128
|
-
5. **Errors propagate
|
|
143
|
+
5. **Errors isolate in `run_chores!`, propagate in `do_chore!`/`do_chores!`.** Single-instance methods let exceptions propagate; the bulk runner rescues per-record and increments the chore's `errors` counter so one failure doesn't halt the run.
|
|
129
144
|
|
|
130
145
|
## Common Patterns
|
|
131
146
|
|
|
@@ -150,7 +165,7 @@ class Customer < Familia::Horreum
|
|
|
150
165
|
end
|
|
151
166
|
end
|
|
152
167
|
|
|
153
|
-
customer.
|
|
168
|
+
customer.do_chores!
|
|
154
169
|
# => { trim_whitespace: true, uppercase_country: nil }
|
|
155
170
|
```
|
|
156
171
|
|
|
@@ -176,28 +191,43 @@ chore :reconcile_billing do |account|
|
|
|
176
191
|
end
|
|
177
192
|
```
|
|
178
193
|
|
|
179
|
-
### Tracking Modified Records
|
|
194
|
+
### Tracking Modified Records (Bulk)
|
|
195
|
+
|
|
196
|
+
`run_chores!` already aggregates `modified` and `errors` counts per chore. Use it directly:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
report = Organization.run_chores!
|
|
200
|
+
report[:chores].each do |name, counts|
|
|
201
|
+
puts "#{name}: #{counts[:modified]} modified, #{counts[:errors]} errors"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Custom Iteration (e.g. SCAN-Based)
|
|
206
|
+
|
|
207
|
+
If `instances`-driven iteration isn't suitable (sharded data, custom scoping), drop down to `do_chore!`/`do_chores!`:
|
|
180
208
|
|
|
181
209
|
```ruby
|
|
182
210
|
modified = []
|
|
183
211
|
Organization.instances.each do |id|
|
|
184
|
-
org = Organization.
|
|
185
|
-
results = org.
|
|
212
|
+
org = Organization.find_by_identifier(id) or next
|
|
213
|
+
results = org.do_chores!
|
|
186
214
|
modified << id if results.values.any?
|
|
187
215
|
end
|
|
188
216
|
puts "Modified #{modified.size} records: #{modified.inspect}"
|
|
189
217
|
```
|
|
190
218
|
|
|
191
|
-
###
|
|
219
|
+
### Wrapping `run_chores!` for a Job Framework
|
|
192
220
|
|
|
193
221
|
```ruby
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
class HousekeepingJob
|
|
223
|
+
def self.perform_for(klass)
|
|
224
|
+
report = klass.run_chores!(batch_size: 50)
|
|
225
|
+
StatsD.gauge("housekeeping.#{klass.name}.scanned", report[:scanned])
|
|
226
|
+
report[:chores].each do |chore, counts|
|
|
227
|
+
StatsD.increment("housekeeping.#{klass.name}.#{chore}.modified", counts[:modified])
|
|
228
|
+
StatsD.increment("housekeeping.#{klass.name}.#{chore}.errors", counts[:errors])
|
|
229
|
+
end
|
|
230
|
+
report
|
|
201
231
|
end
|
|
202
232
|
end
|
|
203
233
|
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Migrating to Familia 2.9.0
|
|
2
|
+
|
|
3
|
+
This version introduces batch iteration primitives for DataTypes, enabling efficient enumeration over large Redis collections. It also includes breaking changes to method names for clarity.
|
|
4
|
+
|
|
5
|
+
## Breaking Changes
|
|
6
|
+
|
|
7
|
+
### Method Renames
|
|
8
|
+
|
|
9
|
+
The multi-field update methods have been renamed to better reflect their purpose:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Before (2.8.x)
|
|
13
|
+
user.batch_update(name: "Alice", email: "alice@example.com")
|
|
14
|
+
user.batch_fast_write(name: "Alice", email: "alice@example.com")
|
|
15
|
+
|
|
16
|
+
# After (2.9.0)
|
|
17
|
+
user.multi_field_update(name: "Alice", email: "alice@example.com")
|
|
18
|
+
user.multi_field_fast_write(name: "Alice", email: "alice@example.com")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Migration**: Find and replace `batch_update` with `multi_field_update` and `batch_fast_write` with `multi_field_fast_write`.
|
|
22
|
+
|
|
23
|
+
### MultiResult Namespace
|
|
24
|
+
|
|
25
|
+
`MultiResult` has moved into the Familia namespace:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Before (2.8.x)
|
|
29
|
+
result.is_a?(MultiResult)
|
|
30
|
+
|
|
31
|
+
# After (2.9.0)
|
|
32
|
+
result.is_a?(Familia::MultiResult)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Migration**: Replace bare `MultiResult` references with `Familia::MultiResult`.
|
|
36
|
+
|
|
37
|
+
## New Features
|
|
38
|
+
|
|
39
|
+
### Enumerable Integration
|
|
40
|
+
|
|
41
|
+
All collection DataTypes now include Ruby's `Enumerable` module, providing `each_slice`, `lazy`, `map`, `reduce`, `find`, and other stdlib methods:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# Lazy iteration with transformation
|
|
45
|
+
Org.instances.lazy.map { |id| id.upcase }.take(10).to_a
|
|
46
|
+
|
|
47
|
+
# Batch processing with each_slice
|
|
48
|
+
User.instances.each_slice(100) do |batch|
|
|
49
|
+
batch.each { |id| process(id) }
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Filtered Iteration
|
|
54
|
+
|
|
55
|
+
Each DataType now supports type-specific filters on `each`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# SortedSet: filter by score (timestamp) bounds
|
|
59
|
+
Org.instances.each(since: 24.hours.ago, until: Time.now) do |id|
|
|
60
|
+
puts id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# HashKey: filter by field name pattern
|
|
64
|
+
user.profile.each(matching: "pref_*") do |field, value|
|
|
65
|
+
puts "#{field}: #{value}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# UnsortedSet: filter by member pattern
|
|
69
|
+
user.tags.each(matching: "admin*") do |tag|
|
|
70
|
+
puts tag
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### each_record for Loading Horreum Instances
|
|
75
|
+
|
|
76
|
+
`each_record` yields fully-loaded Horreum records instead of raw IDs:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# Load records in batches of 100
|
|
80
|
+
Org.instances.each_record(batch_size: 100) do |org|
|
|
81
|
+
org.tidy! # org is a loaded Horreum instance
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Control pipelining depth separately from fetch batch size
|
|
85
|
+
Org.instances.each_record(batch_size: 500, write_size: 50) do |org|
|
|
86
|
+
org.status!("active")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Serial execution (no pipelining)
|
|
90
|
+
Org.instances.each_record(batch_size: 100, write_size: nil) do |org|
|
|
91
|
+
org.complex_operation
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Ghost instances (keys that expired but remain in the `instances` sorted set) are automatically filtered and never reach the block.
|
|
96
|
+
|
|
97
|
+
### BatchResult for Aggregated Operations
|
|
98
|
+
|
|
99
|
+
`Familia::BatchResult` aggregates results from batch operations with per-record error isolation:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
result = Familia::BatchResult.collect(
|
|
103
|
+
Org.instances.each_record(batch_size: 100, since: 24.hours.ago)
|
|
104
|
+
) do |org|
|
|
105
|
+
org.tidy!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
result.scanned # Total records yielded to block
|
|
109
|
+
result.modified # Count of truthy block returns
|
|
110
|
+
result.errors # Array of {id:, error:} for failed records
|
|
111
|
+
result.duration_ms # Total execution time
|
|
112
|
+
|
|
113
|
+
# Re-raise errors after completion
|
|
114
|
+
result = Familia::BatchResult.collect(enum, strict: true) { |r| r.process! }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Concurrent Mutation Behavior
|
|
118
|
+
|
|
119
|
+
When iterating with `each` or `each_record`, be aware of Redis cursor semantics:
|
|
120
|
+
|
|
121
|
+
- Items present from iteration start to end are guaranteed to be returned
|
|
122
|
+
- Items added or removed mid-iteration may or may not appear
|
|
123
|
+
- Blocks should be idempotent to handle potential duplicates
|
|
124
|
+
|
|
125
|
+
This is inherent to ZSCAN/HSCAN/SSCAN and is documented, not a bug.
|
data/familia.gemspec
CHANGED
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.add_dependency 'json_schemer', '~> 2.0'
|
|
26
26
|
spec.add_dependency 'logger', '~> 1.7'
|
|
27
27
|
spec.add_dependency 'oj', '~> 3.16'
|
|
28
|
-
spec.add_dependency 'redis', '>=
|
|
28
|
+
spec.add_dependency 'redis', '>= 5.0', '< 6.0'
|
|
29
29
|
spec.add_dependency 'stringio', '~> 3.1.1'
|
|
30
30
|
spec.add_dependency 'uri-valkey', '~> 1.4'
|
|
31
31
|
|