foobara 0.0.129 → 0.0.131
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.md +11 -0
- data/README.md +3 -3
- data/projects/builtin_types/src/attributes/supported_transformers/defaults.rb +2 -2
- data/projects/command_connectors/src/serializers/yaml_serializer.rb +2 -0
- data/projects/entity/src/concerns/initialization.rb +4 -1
- data/projects/in_memory_crud_driver_minimal/src/in_memory_minimal.rb +1 -11
- data/projects/namespace/src/is_namespace.rb +1 -1
- data/projects/persistence/src/entity_attributes_crud_driver.rb +68 -31
- data/projects/persistence/src/entity_base/transaction/concerns/state_transitions.rb +47 -5
- data/projects/persistence/src/entity_base/transaction/state_machine.rb +4 -1
- data/projects/persistence/src/entity_base/transaction.rb +5 -1
- data/projects/persistence/src/entity_base/transaction_table/concerns/record_tracking.rb +16 -1
- data/projects/persistence/src/entity_base/transaction_table.rb +10 -0
- data/projects/persistence/src/entity_base.rb +52 -7
- data/projects/weak_object_set/src/weak_object_set.rb +48 -14
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c95217bebc01ab27c28cdf572b5e19eb2902fd68b1bb4d7328c093b02cfe8dbf
|
4
|
+
data.tar.gz: 76d32046c22f96ebf9e73d738aa4154fea3edb740e66fab71954abbd8594a35c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 148fe1548cc4731673ffa299fe1d5ef96a8377cdbc613e3aeff989b16efbc250beea9b58f1b3b1727d0b399b5f1925186c1ac97908c8e2bf6113f4ff0cba1835
|
7
|
+
data.tar.gz: e49bad06d6f305b4e1716fd56fa47298028c6b699977e95417e9013c95f3a25a90ca8a9b2d96b8a11c9fb5989b56587ee87d7b719d2397b90d95210168a594ce
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# [0.0.131] - 2025-06-16
|
2
|
+
|
3
|
+
- Extract InMemoryMinimal crud driver specs to foobara-crud-driver-spec-helpers gem
|
4
|
+
- Better support for nested transactions
|
5
|
+
- Fix buggy actions carried out when committing/rolling back transactions
|
6
|
+
- Fix thread leaks in WeakObjectSet and test suite
|
7
|
+
|
8
|
+
# [0.0.130] - 2025-06-06
|
9
|
+
|
10
|
+
- Support using a proc as an attributes default for lazy evaluation of default values
|
11
|
+
|
1
12
|
# [0.0.129] - 2025-05-30
|
2
13
|
|
3
14
|
- Store the raw, unprocessed, command result for debugging/introspection
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@ domain operations in commands, and automatically expose machine-readable formal
|
|
3
3
|
commands so that integration code can be decoupled and abstracted away.
|
4
4
|
|
5
5
|
This, as well as some other features of foobara, help manage domain complexity and produce
|
6
|
-
more flexible systems
|
6
|
+
more flexible systems.
|
7
7
|
|
8
8
|
You can watch a video that gives a good overview of what Foobara is and its goals here:
|
9
9
|
[Introduction to the Foobara software framework](https://youtu.be/SSOmQqjNSVY)
|
@@ -27,7 +27,7 @@ You can watch a video that gives a good overview of what Foobara is and its goal
|
|
27
27
|
* [HTTP Command Connectors](#http-command-connectors)
|
28
28
|
* [Rack Connector](#rack-connector)
|
29
29
|
* [Rails Connector](#rails-connector)
|
30
|
-
|
30
|
+
* [MCP Command Connector](#mcp-command-connector)
|
31
31
|
* [Async Command Connectors](#async-command-connectors)
|
32
32
|
* [Scheduler Command Connectors](#scheduler-command-connectors)
|
33
33
|
* [Intermediate Foobara](#intermediate-foobara)
|
@@ -834,7 +834,7 @@ end
|
|
834
834
|
|
835
835
|
This has the same effect as the previous code and is just a stylistic alternative.
|
836
836
|
|
837
|
-
|
837
|
+
#### MCP Command Connector
|
838
838
|
|
839
839
|
We can have an MCP server for free for our commands. Let's try it!
|
840
840
|
|
@@ -20,11 +20,11 @@ module Foobara
|
|
20
20
|
allow_nil = parent_declaration_data[:element_type_declarations][attribute_name][:allow_nil]
|
21
21
|
|
22
22
|
unless allow_nil
|
23
|
-
to_apply[attribute_name] = default
|
23
|
+
to_apply[attribute_name] = default.is_a?(Proc) ? default.call : default
|
24
24
|
end
|
25
25
|
end
|
26
26
|
else
|
27
|
-
to_apply[attribute_name] = default
|
27
|
+
to_apply[attribute_name] = default.is_a?(Proc) ? default.call : default
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -86,7 +86,10 @@ module Foobara
|
|
86
86
|
|
87
87
|
defaults = attributes_type.declaration_data[:defaults]
|
88
88
|
if defaults && !defaults.empty?
|
89
|
-
|
89
|
+
resolved_defaults = defaults.transform_values do |value|
|
90
|
+
value.is_a?(Proc) ? value.call : value
|
91
|
+
end
|
92
|
+
record.write_attributes_without_callbacks(resolved_defaults)
|
90
93
|
end
|
91
94
|
|
92
95
|
record.write_attributes_without_callbacks(attributes)
|
@@ -20,7 +20,7 @@ module Foobara
|
|
20
20
|
# TODO: all multiple record methods should return enumerators and code further up should only use
|
21
21
|
# the lazy enumerator interface... to encourage that/catch bugs we will return lazy enumerators in these
|
22
22
|
# built-in crud drivers
|
23
|
-
def all
|
23
|
+
def all(page_size: nil)
|
24
24
|
records.each_value.lazy
|
25
25
|
end
|
26
26
|
|
@@ -32,16 +32,6 @@ module Foobara
|
|
32
32
|
Util.deep_dup(records[record_id])
|
33
33
|
end
|
34
34
|
|
35
|
-
def find!(record_id)
|
36
|
-
attributes = find(record_id)
|
37
|
-
|
38
|
-
unless attributes
|
39
|
-
raise CannotFindError.new(record_id, "does not exist")
|
40
|
-
end
|
41
|
-
|
42
|
-
attributes
|
43
|
-
end
|
44
|
-
|
45
35
|
def insert(attributes)
|
46
36
|
attributes = Util.deep_dup(attributes)
|
47
37
|
|
@@ -273,7 +273,7 @@ module Foobara
|
|
273
273
|
filter, method, bang = _filter_from_method_name(method_name)
|
274
274
|
|
275
275
|
if filter
|
276
|
-
method = "foobara_#{method}#{
|
276
|
+
method = "foobara_#{method}#{"!" if bang}"
|
277
277
|
send(method, *, **, filter:, &)
|
278
278
|
else
|
279
279
|
# :nocov:
|
@@ -4,6 +4,12 @@ module Foobara
|
|
4
4
|
class EntityAttributesCrudDriver
|
5
5
|
attr_accessor :raw_connection, :tables
|
6
6
|
|
7
|
+
class << self
|
8
|
+
def has_real_transactions?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
def initialize(connection_or_credentials = nil)
|
8
14
|
self.raw_connection = open_connection(connection_or_credentials)
|
9
15
|
self.tables = {}
|
@@ -31,7 +37,7 @@ module Foobara
|
|
31
37
|
def rollback_transaction(_raw_tx)
|
32
38
|
end
|
33
39
|
|
34
|
-
def
|
40
|
+
def commit_transaction(_raw_tx)
|
35
41
|
end
|
36
42
|
|
37
43
|
def table_for(entity_class)
|
@@ -91,7 +97,7 @@ module Foobara
|
|
91
97
|
# :nocov:
|
92
98
|
end
|
93
99
|
|
94
|
-
def all
|
100
|
+
def all(page_size: nil)
|
95
101
|
# :nocov:
|
96
102
|
raise "subclass responsibility"
|
97
103
|
# :nocov:
|
@@ -107,10 +113,14 @@ module Foobara
|
|
107
113
|
# :nocov:
|
108
114
|
end
|
109
115
|
|
110
|
-
def find!(
|
111
|
-
|
112
|
-
|
113
|
-
|
116
|
+
def find!(record_id)
|
117
|
+
attributes = find(record_id)
|
118
|
+
|
119
|
+
unless attributes
|
120
|
+
raise CannotFindError.new(record_id, "does not exist")
|
121
|
+
end
|
122
|
+
|
123
|
+
attributes
|
114
124
|
end
|
115
125
|
|
116
126
|
def find_many!(record_ids)
|
@@ -157,39 +167,22 @@ module Foobara
|
|
157
167
|
|
158
168
|
def matches_attributes_filter?(attributes, attributes_filter)
|
159
169
|
attributes_filter.all? do |attribute_name_or_path, value|
|
160
|
-
|
170
|
+
# get the model-free type?
|
171
|
+
attribute_type = entity_class.attributes_type.type_at_path(attribute_name_or_path)
|
172
|
+
|
173
|
+
value = restore_attributes(value, attribute_type)
|
161
174
|
|
162
175
|
if attribute_name_or_path.is_a?(::Array)
|
163
176
|
values = DataPath.values_at(attribute_name_or_path, attributes)
|
164
177
|
|
165
178
|
values.any? do |attribute_value|
|
166
|
-
|
179
|
+
restore_attributes(attribute_value, attribute_type) == value
|
167
180
|
end
|
168
181
|
else
|
169
|
-
attribute_value = attributes
|
170
|
-
|
171
|
-
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def normalize_attribute_filter_value(value)
|
176
|
-
case value
|
177
|
-
when ::Array
|
178
|
-
value.map { |v| normalize_attribute_filter_value(v) }
|
179
|
-
when ::Hash
|
180
|
-
value.to_h do |k, v|
|
181
|
-
[normalize_attribute_filter_value(k), normalize_attribute_filter_value(v)]
|
182
|
-
end
|
183
|
-
when DetachedEntity
|
184
|
-
if value.persisted?
|
185
|
-
normalize_attribute_filter_value(value.primary_key)
|
186
|
-
else
|
187
|
-
value
|
182
|
+
attribute_value = DataPath.value_at(attribute_name_or_path, attributes)
|
183
|
+
attribute_value = restore_attributes(attribute_value, attribute_type)
|
184
|
+
attribute_value == value
|
188
185
|
end
|
189
|
-
when Model
|
190
|
-
normalize_attribute_filter_value(value.attributes)
|
191
|
-
else
|
192
|
-
value
|
193
186
|
end
|
194
187
|
end
|
195
188
|
|
@@ -258,6 +251,50 @@ module Foobara
|
|
258
251
|
def primary_key_attribute
|
259
252
|
entity_class.primary_key_attribute
|
260
253
|
end
|
254
|
+
|
255
|
+
def restore_attributes(object, type = entity_class.attributes_type)
|
256
|
+
if type.extends?(BuiltinTypes[:attributes])
|
257
|
+
object.to_h do |attribute_name, attribute_value|
|
258
|
+
attribute_type = type.type_at_path(attribute_name)
|
259
|
+
[attribute_name.to_sym, restore_attributes(attribute_value, attribute_type)]
|
260
|
+
end
|
261
|
+
elsif type.extends?(BuiltinTypes[:tuple])
|
262
|
+
# TODO: test this code path
|
263
|
+
# :nocov:
|
264
|
+
object.map.with_index do |value, index|
|
265
|
+
element_type = type.element_types[index]
|
266
|
+
restore_attributes(value, element_type)
|
267
|
+
end
|
268
|
+
# :nocov:
|
269
|
+
elsif type.extends?(BuiltinTypes[:array])
|
270
|
+
element_type = type.element_type
|
271
|
+
object.map { |value| restore_attributes(value, element_type) }
|
272
|
+
elsif type.extends?(BuiltinTypes[:entity])
|
273
|
+
if object.is_a?(Model)
|
274
|
+
if object.persisted?
|
275
|
+
object = object.primary_key
|
276
|
+
restore_attributes(object, type.target_class.primary_key_type)
|
277
|
+
else
|
278
|
+
object
|
279
|
+
end
|
280
|
+
else
|
281
|
+
restore_attributes(object, type.target_class.primary_key_type)
|
282
|
+
end
|
283
|
+
elsif type.extends?(BuiltinTypes[:model])
|
284
|
+
if object.is_a?(Model)
|
285
|
+
object = object.attributes
|
286
|
+
end
|
287
|
+
restore_attributes(object, type.element_types)
|
288
|
+
else
|
289
|
+
outcome = type.process_value(object)
|
290
|
+
|
291
|
+
if outcome.success?
|
292
|
+
outcome.result
|
293
|
+
else
|
294
|
+
object
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
261
298
|
end
|
262
299
|
end
|
263
300
|
end
|
@@ -15,12 +15,20 @@ module Foobara
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
def open_nested!(outer_tx)
|
19
|
+
state_machine.open_nested! do
|
20
|
+
self.is_nested = true
|
21
|
+
self.raw_tx = outer_tx.raw_tx
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
18
25
|
def flush!
|
19
26
|
state_machine.flush! do
|
20
27
|
each_table(&:validate!)
|
21
28
|
each_table(&:flush_created!)
|
22
29
|
each_table(&:flush_updated_and_hard_deleted!)
|
23
30
|
end
|
31
|
+
entity_attributes_crud_driver.flush_transaction(raw_tx)
|
24
32
|
rescue => e
|
25
33
|
# :nocov:
|
26
34
|
rollback!(e)
|
@@ -34,6 +42,7 @@ module Foobara
|
|
34
42
|
state_machine.revert! do
|
35
43
|
each_table(&:revert!)
|
36
44
|
end
|
45
|
+
entity_attributes_crud_driver.revert_transaction(raw_tx)
|
37
46
|
rescue => e
|
38
47
|
# :nocov:
|
39
48
|
rollback!(e)
|
@@ -42,11 +51,25 @@ module Foobara
|
|
42
51
|
end
|
43
52
|
|
44
53
|
def commit!
|
54
|
+
return commit_nested! if nested?
|
55
|
+
|
45
56
|
state_machine.commit! do
|
46
|
-
each_table(&:
|
47
|
-
|
48
|
-
each_table(&:
|
49
|
-
|
57
|
+
each_table(&:commit!)
|
58
|
+
entity_attributes_crud_driver.commit_transaction(raw_tx)
|
59
|
+
each_table(&:transaction_closed)
|
60
|
+
end
|
61
|
+
rescue => e
|
62
|
+
# :nocov:
|
63
|
+
rollback!(e)
|
64
|
+
raise
|
65
|
+
# :nocov:
|
66
|
+
end
|
67
|
+
|
68
|
+
def commit_nested!
|
69
|
+
state_machine.commit_nested! do
|
70
|
+
each_table(&:commit!)
|
71
|
+
entity_attributes_crud_driver.flush_transaction(raw_tx)
|
72
|
+
each_table(&:transaction_closed)
|
50
73
|
end
|
51
74
|
rescue => e
|
52
75
|
# :nocov:
|
@@ -61,13 +84,32 @@ module Foobara
|
|
61
84
|
end
|
62
85
|
|
63
86
|
def rollback!(because_of = nil)
|
87
|
+
return rollback_nested!(because_of) if nested?
|
88
|
+
|
64
89
|
state_machine.rollback! do
|
65
90
|
# TODO: raise error if already flushed and if crud_driver doesn't support true transactions
|
66
91
|
entity_attributes_crud_driver.rollback_transaction(raw_tx)
|
67
92
|
each_table(&:rollback!)
|
68
|
-
entity_attributes_crud_driver.close_transaction(raw_tx)
|
69
93
|
end
|
70
94
|
|
95
|
+
each_table(&:transaction_closed)
|
96
|
+
|
97
|
+
if !because_of && (self == entity_base.current_transaction)
|
98
|
+
raise RolledBack, "intentionally rolled back"
|
99
|
+
end
|
100
|
+
rescue
|
101
|
+
state_machine.error! if state_machine.currently_open?
|
102
|
+
raise
|
103
|
+
end
|
104
|
+
|
105
|
+
def rollback_nested!(because_of = nil)
|
106
|
+
state_machine.rollback_nested! do
|
107
|
+
entity_attributes_crud_driver.revert_transaction(raw_tx)
|
108
|
+
each_table(&:revert!)
|
109
|
+
end
|
110
|
+
|
111
|
+
each_table(&:transaction_closed)
|
112
|
+
|
71
113
|
if !because_of && (self == entity_base.current_transaction)
|
72
114
|
raise RolledBack, "intentionally rolled back"
|
73
115
|
end
|
@@ -7,6 +7,7 @@ module Foobara
|
|
7
7
|
set_transition_map({
|
8
8
|
unopened: {
|
9
9
|
open: :open,
|
10
|
+
open_nested: :open,
|
10
11
|
close: :closed
|
11
12
|
},
|
12
13
|
open: {
|
@@ -17,7 +18,9 @@ module Foobara
|
|
17
18
|
# TODO: should we have intermediate states to quickly get out of the open state?
|
18
19
|
rollback: :closed,
|
19
20
|
commit: :closed,
|
20
|
-
error: :closed
|
21
|
+
error: :closed,
|
22
|
+
commit_nested: :closed,
|
23
|
+
rollback_nested: :closed
|
21
24
|
}
|
22
25
|
})
|
23
26
|
end
|
@@ -6,7 +6,7 @@ module Foobara
|
|
6
6
|
include Concerns::EntityCallbackHandling
|
7
7
|
include Concerns::TransactionTracking
|
8
8
|
|
9
|
-
attr_accessor :state_machine, :entity_base, :raw_tx, :tables
|
9
|
+
attr_accessor :state_machine, :entity_base, :raw_tx, :tables, :is_nested
|
10
10
|
|
11
11
|
def initialize(entity_base)
|
12
12
|
self.entity_base = entity_base
|
@@ -162,6 +162,10 @@ module Foobara
|
|
162
162
|
def perform(&)
|
163
163
|
entity_base.using_transaction(self, &)
|
164
164
|
end
|
165
|
+
|
166
|
+
def nested?
|
167
|
+
is_nested
|
168
|
+
end
|
165
169
|
end
|
166
170
|
end
|
167
171
|
end
|
@@ -83,10 +83,25 @@ module Foobara
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def rolled_back
|
86
|
+
closed
|
87
|
+
end
|
88
|
+
|
89
|
+
def committed
|
90
|
+
closed
|
91
|
+
end
|
92
|
+
|
93
|
+
def closed
|
86
94
|
marked_hard_deleted.clear
|
87
95
|
marked_updated.clear
|
88
96
|
marked_created.clear
|
89
|
-
|
97
|
+
marked_loading.clear
|
98
|
+
end
|
99
|
+
|
100
|
+
# We need to clear this one separately. That's because otherwise a different table
|
101
|
+
# might flush and create a thunk if it has an association to this table but we've stopped
|
102
|
+
# tracking the record.
|
103
|
+
def transaction_closed
|
104
|
+
tracked_records.close
|
90
105
|
end
|
91
106
|
|
92
107
|
def reverted
|
@@ -86,6 +86,7 @@ module Foobara
|
|
86
86
|
# :nocov:
|
87
87
|
end
|
88
88
|
|
89
|
+
# rubocop:disable Lint/IdentityComparison
|
89
90
|
if entity &&
|
90
91
|
(!entity.equal?(entity_or_record_id) || entity.object_id != entity_or_record_id.object_id)
|
91
92
|
# :nocov:
|
@@ -93,6 +94,7 @@ module Foobara
|
|
93
94
|
"Try passing in the primary key instead of constructing an unloaded entity to pass in."
|
94
95
|
# :nocov:
|
95
96
|
end
|
97
|
+
# rubocop:enable Lint/IdentityComparison
|
96
98
|
|
97
99
|
record_id = entity.primary_key
|
98
100
|
else
|
@@ -630,6 +632,14 @@ module Foobara
|
|
630
632
|
rolled_back
|
631
633
|
end
|
632
634
|
|
635
|
+
def commit!
|
636
|
+
validate!
|
637
|
+
flush_created!
|
638
|
+
flush_updated_and_hard_deleted!
|
639
|
+
|
640
|
+
committed
|
641
|
+
end
|
642
|
+
|
633
643
|
def revert!
|
634
644
|
# TODO: could pause record tracking while doing this as a performance boost
|
635
645
|
marked_updated.each(&:restore_without_callbacks!)
|
@@ -52,7 +52,31 @@ module Foobara
|
|
52
52
|
Thread.inheritable_thread_local_var_set(transaction_key, transaction)
|
53
53
|
end
|
54
54
|
|
55
|
-
|
55
|
+
# What types of transaction scenarios are there?
|
56
|
+
# 1. If a transaction is already open, use it as a "nested transaction", otherwise, open a new one.
|
57
|
+
# A nested transaction means that "rollback" is the same as "revert" and "commit" is the same as "flush".
|
58
|
+
# For a true
|
59
|
+
# 2. If a transaction is already open, raise an error. otherwise, open a new one.
|
60
|
+
# 3. Open a new, independent transaction, no matter what.
|
61
|
+
# 4. If a transaction is already open, use it, otherwise, open a new one.
|
62
|
+
# 5. We are outside of a transaction but have a handle on one. We want to set it as the current transaction.
|
63
|
+
# and do some work in that transaction.
|
64
|
+
# Which use cases do we probably need at the moment?
|
65
|
+
# 1. If we are running a command calling other commands, we will open transactions when needed but
|
66
|
+
# inherit any already-open transactions. Commands don't commit or flush to the already-open transactions
|
67
|
+
# that they inherit. So this feels like a "use existing" situation or a situation where we don't even
|
68
|
+
# bother calling open_transaction at all. This is the most important use-case. It can be helpful to raise
|
69
|
+
# in this situation because it is not expected that there's an existing transaction yet we're opening another.
|
70
|
+
# 2. We might have a situation where we are in one transaction but definitely want to open a new one and
|
71
|
+
# commit it ourselves and have its results committed and visible independent of the current transaction.
|
72
|
+
# So this feels like a "open new" situation where we don't want to raise an error if a transaction is
|
73
|
+
# already open.
|
74
|
+
VALID_MODES = [
|
75
|
+
:use_existing,
|
76
|
+
:open_nested,
|
77
|
+
:open_new,
|
78
|
+
nil
|
79
|
+
].freeze
|
56
80
|
|
57
81
|
def using_transaction(existing_transaction, &)
|
58
82
|
transaction(existing_transaction:, &)
|
@@ -67,18 +91,28 @@ module Foobara
|
|
67
91
|
|
68
92
|
old_transaction = current_transaction
|
69
93
|
|
70
|
-
if old_transaction
|
94
|
+
if old_transaction && !old_transaction.currently_open?
|
71
95
|
old_transaction = nil
|
72
96
|
end
|
73
97
|
|
74
|
-
|
98
|
+
open_nested = false
|
99
|
+
|
100
|
+
if old_transaction
|
75
101
|
if mode == :use_existing || existing_transaction == old_transaction
|
76
102
|
if block_given?
|
77
103
|
return yield old_transaction
|
78
104
|
else
|
79
105
|
return old_transaction
|
80
106
|
end
|
81
|
-
elsif mode
|
107
|
+
elsif mode == :open_nested
|
108
|
+
open_nested = true
|
109
|
+
elsif mode == :open_new
|
110
|
+
if existing_transaction
|
111
|
+
# :nocov:
|
112
|
+
raise ArgumentError, "Cannot use mode :open_new with existing_transaction:"
|
113
|
+
# :nocov:
|
114
|
+
end
|
115
|
+
else
|
82
116
|
# :nocov:
|
83
117
|
raise "Transaction already open. " \
|
84
118
|
"Use mode :use_existing if you want to make use of the existing transaction. " \
|
@@ -96,16 +130,27 @@ module Foobara
|
|
96
130
|
tx = existing_transaction
|
97
131
|
else
|
98
132
|
tx = Transaction.new(self)
|
99
|
-
|
133
|
+
|
134
|
+
if open_nested
|
135
|
+
tx.open_nested!(old_transaction)
|
136
|
+
else
|
137
|
+
tx.open!
|
138
|
+
end
|
100
139
|
end
|
101
140
|
|
102
141
|
set_current_transaction(tx)
|
142
|
+
|
103
143
|
result = yield tx
|
104
|
-
|
144
|
+
|
145
|
+
if tx.currently_open? && !existing_transaction
|
146
|
+
tx.commit!
|
147
|
+
end
|
105
148
|
result
|
106
149
|
rescue Foobara::Persistence::EntityBase::Transaction::RolledBack # rubocop:disable Lint/SuppressedException
|
107
150
|
rescue => e
|
108
|
-
|
151
|
+
if tx.currently_open?
|
152
|
+
tx.rollback!(e)
|
153
|
+
end
|
109
154
|
raise
|
110
155
|
ensure
|
111
156
|
set_current_transaction(old_transaction)
|
@@ -4,8 +4,6 @@ module Foobara
|
|
4
4
|
# TODO: a possible optimization: have a certain number of records before the Weakref approach kicks in
|
5
5
|
# that way we don't just immediately clear out useful information without any actual memory burden
|
6
6
|
class WeakObjectSet
|
7
|
-
class InvalidWtf < StandardError; end
|
8
|
-
|
9
7
|
class GarbageCleaner
|
10
8
|
attr_accessor :weak_object_set, :deactivated, :queue, :cleanup_thread
|
11
9
|
|
@@ -17,6 +15,12 @@ module Foobara
|
|
17
15
|
end
|
18
16
|
|
19
17
|
def cleanup_proc
|
18
|
+
if deactivated?
|
19
|
+
# :nocov:
|
20
|
+
raise "GarbageCleaner has been deactivated"
|
21
|
+
# :nocov:
|
22
|
+
end
|
23
|
+
|
20
24
|
@cleanup_proc ||= begin
|
21
25
|
queue = self.queue
|
22
26
|
|
@@ -35,6 +39,8 @@ module Foobara
|
|
35
39
|
end
|
36
40
|
|
37
41
|
def start_cleanup_thread
|
42
|
+
queue = self.queue
|
43
|
+
|
38
44
|
self.cleanup_thread = Thread.new do
|
39
45
|
loop do
|
40
46
|
object_id = queue.pop
|
@@ -57,9 +63,16 @@ module Foobara
|
|
57
63
|
end
|
58
64
|
|
59
65
|
def deactivate
|
66
|
+
raise if deactivated?
|
67
|
+
|
60
68
|
self.deactivated = true
|
61
69
|
queue.close
|
70
|
+
# TODO: don't bother to join here outside of test suite
|
62
71
|
cleanup_thread.join # just doing this for test suite/simplecov
|
72
|
+
@cleanup_proc = nil
|
73
|
+
@queue = nil
|
74
|
+
@weak_object_set = nil
|
75
|
+
@cleanup_thread = nil
|
63
76
|
end
|
64
77
|
|
65
78
|
def deactivated?
|
@@ -69,7 +82,7 @@ module Foobara
|
|
69
82
|
|
70
83
|
include Enumerable
|
71
84
|
|
72
|
-
attr_accessor :monitor, :key_method, :key_to_object_id, :object_id_to_key, :objects
|
85
|
+
attr_accessor :monitor, :key_method, :key_to_object_id, :object_id_to_key, :objects, :closed
|
73
86
|
attr_writer :garbage_cleaner
|
74
87
|
|
75
88
|
def initialize(key_method = nil)
|
@@ -136,19 +149,17 @@ module Foobara
|
|
136
149
|
@garbage_cleaner ||= begin
|
137
150
|
queue = Queue.new
|
138
151
|
|
139
|
-
|
140
|
-
|
141
|
-
ObjectSpace.define_finalizer gc do
|
142
|
-
# :nocov:
|
143
|
-
queue.close
|
144
|
-
# :nocov:
|
145
|
-
end
|
146
|
-
|
147
|
-
gc
|
152
|
+
GarbageCleaner.new(self, queue)
|
148
153
|
end
|
149
154
|
end
|
150
155
|
|
151
156
|
def <<(object)
|
157
|
+
if closed?
|
158
|
+
# :nocov:
|
159
|
+
raise "Cannot add objects to a closed WeakObjectSet"
|
160
|
+
# :nocov:
|
161
|
+
end
|
162
|
+
|
152
163
|
object_id = object.object_id
|
153
164
|
|
154
165
|
monitor.synchronize do
|
@@ -235,11 +246,30 @@ module Foobara
|
|
235
246
|
end
|
236
247
|
end
|
237
248
|
|
249
|
+
def close
|
250
|
+
raise if closed?
|
251
|
+
|
252
|
+
self.closed = true
|
253
|
+
stop_garbage_cleaner
|
254
|
+
end
|
255
|
+
|
256
|
+
def stop_garbage_cleaner
|
257
|
+
gc = nil
|
258
|
+
|
259
|
+
monitor.synchronize do
|
260
|
+
if @garbage_cleaner
|
261
|
+
gc = garbage_cleaner
|
262
|
+
self.garbage_cleaner = nil
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
gc&.deactivate
|
267
|
+
end
|
268
|
+
|
238
269
|
def clear
|
239
270
|
monitor.synchronize do
|
240
|
-
|
271
|
+
stop_garbage_cleaner
|
241
272
|
|
242
|
-
self.garbage_cleaner = nil
|
243
273
|
self.objects = {}
|
244
274
|
|
245
275
|
if key_method
|
@@ -248,5 +278,9 @@ module Foobara
|
|
248
278
|
end
|
249
279
|
end
|
250
280
|
end
|
281
|
+
|
282
|
+
def closed?
|
283
|
+
closed
|
284
|
+
end
|
251
285
|
end
|
252
286
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foobara
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.131
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miles Georgi
|
@@ -41,16 +41,16 @@ dependencies:
|
|
41
41
|
name: foobara-util
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- - "
|
44
|
+
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: 0.0
|
46
|
+
version: 1.0.0
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- - "
|
51
|
+
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 0.0
|
53
|
+
version: 1.0.0
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: inheritable-thread-vars
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -486,8 +486,8 @@ licenses:
|
|
486
486
|
- MPL-2.0
|
487
487
|
metadata:
|
488
488
|
homepage_uri: https://foobara.com
|
489
|
-
source_code_uri: https://
|
490
|
-
changelog_uri: https://
|
489
|
+
source_code_uri: https://github.com/foobara/foobara
|
490
|
+
changelog_uri: https://github.com/foobara/foobara/blob/main/CHANGELOG.md
|
491
491
|
rubygems_mfa_required: 'true'
|
492
492
|
rdoc_options: []
|
493
493
|
require_paths:
|
@@ -529,7 +529,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
529
529
|
- !ruby/object:Gem::Version
|
530
530
|
version: '0'
|
531
531
|
requirements: []
|
532
|
-
rubygems_version: 3.6.
|
532
|
+
rubygems_version: 3.6.9
|
533
533
|
specification_version: 4
|
534
534
|
summary: A command-centric and discoverable software framework with a focus on domain
|
535
535
|
concepts and abstracting away integration code
|