dexkit 0.1.0 → 0.2.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/CHANGELOG.md +34 -0
- data/README.md +55 -1
- data/guides/llm/EVENT.md +300 -0
- data/lib/dex/concern.rb +10 -0
- data/lib/dex/event/bus.rb +98 -0
- data/lib/dex/event/execution_state.rb +17 -0
- data/lib/dex/event/handler.rb +77 -0
- data/lib/dex/event/metadata.rb +54 -0
- data/lib/dex/event/processor.rb +61 -0
- data/lib/dex/event/suppression.rb +49 -0
- data/lib/dex/event/trace.rb +56 -0
- data/lib/dex/event.rb +87 -0
- data/lib/dex/event_test_helpers/assertions.rb +70 -0
- data/lib/dex/event_test_helpers.rb +88 -0
- data/lib/dex/operation/async_proxy.rb +30 -36
- data/lib/dex/operation/async_wrapper.rb +3 -19
- data/lib/dex/operation/callback_wrapper.rb +11 -15
- data/lib/dex/operation/jobs.rb +8 -14
- data/lib/dex/operation/lock_wrapper.rb +2 -11
- data/lib/dex/operation/pipeline.rb +5 -5
- data/lib/dex/operation/record_wrapper.rb +10 -38
- data/lib/dex/operation/rescue_wrapper.rb +1 -3
- data/lib/dex/operation/result_wrapper.rb +7 -14
- data/lib/dex/operation/settings.rb +10 -3
- data/lib/dex/operation/transaction_wrapper.rb +7 -20
- data/lib/dex/operation.rb +54 -105
- data/lib/dex/{operation/props_setup.rb → props_setup.rb} +12 -15
- data/lib/dex/test_helpers.rb +3 -1
- data/lib/dex/type_coercion.rb +96 -0
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +14 -1
- metadata +15 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Event
|
|
5
|
+
# Lazy-loaded ActiveJob processor (same pattern as Operation::DirectJob)
|
|
6
|
+
def self.const_missing(name)
|
|
7
|
+
return super unless name == :Processor && defined?(ActiveJob::Base)
|
|
8
|
+
|
|
9
|
+
const_set(:Processor, Class.new(ActiveJob::Base) do
|
|
10
|
+
def perform(handler_class:, event_class:, payload:, metadata:, trace: nil, context: nil, attempt_number: 1)
|
|
11
|
+
restore_context(context)
|
|
12
|
+
|
|
13
|
+
handler = Object.const_get(handler_class)
|
|
14
|
+
retry_config = handler._event_handler_retry_config
|
|
15
|
+
|
|
16
|
+
Dex::Event::Trace.restore(trace) do
|
|
17
|
+
handler._event_handle_from_payload(event_class, payload, metadata)
|
|
18
|
+
end
|
|
19
|
+
rescue => _e
|
|
20
|
+
if retry_config && attempt_number <= retry_config[:count]
|
|
21
|
+
delay = compute_delay(retry_config, attempt_number)
|
|
22
|
+
self.class.set(wait: delay).perform_later(
|
|
23
|
+
handler_class: handler_class,
|
|
24
|
+
event_class: event_class,
|
|
25
|
+
payload: payload,
|
|
26
|
+
metadata: metadata,
|
|
27
|
+
trace: trace,
|
|
28
|
+
context: context,
|
|
29
|
+
attempt_number: attempt_number + 1
|
|
30
|
+
)
|
|
31
|
+
else
|
|
32
|
+
raise
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def restore_context(context)
|
|
39
|
+
return unless context
|
|
40
|
+
|
|
41
|
+
restorer = Dex.configuration.restore_event_context
|
|
42
|
+
return unless restorer
|
|
43
|
+
|
|
44
|
+
restorer.call(context)
|
|
45
|
+
rescue => e
|
|
46
|
+
Dex::Event._warn("restore_event_context failed: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def compute_delay(config, attempt)
|
|
50
|
+
wait = config[:wait]
|
|
51
|
+
case wait
|
|
52
|
+
when Numeric then wait
|
|
53
|
+
when Proc then wait.call(attempt)
|
|
54
|
+
else
|
|
55
|
+
2**(attempt - 1) # exponential backoff
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Event
|
|
5
|
+
module Suppression
|
|
6
|
+
SUPPRESSED_KEY = :_dex_event_suppressed
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
include ExecutionState
|
|
10
|
+
|
|
11
|
+
def suppress(*classes, &block)
|
|
12
|
+
previous = _suppressed_set
|
|
13
|
+
new_set = previous.dup
|
|
14
|
+
if classes.empty?
|
|
15
|
+
new_set << :all
|
|
16
|
+
else
|
|
17
|
+
classes.each do |klass|
|
|
18
|
+
Event.validate_event_class!(klass)
|
|
19
|
+
new_set << klass
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
_set_suppressed(new_set)
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
_set_suppressed(previous)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def suppressed?(event_class)
|
|
29
|
+
set = _suppressed_set
|
|
30
|
+
set.include?(:all) || set.any? { |k| k != :all && event_class <= k }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear!
|
|
34
|
+
_execution_state[SUPPRESSED_KEY] = Set.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def _suppressed_set
|
|
40
|
+
_execution_state[SUPPRESSED_KEY] ||= Set.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def _set_suppressed(set)
|
|
44
|
+
_execution_state[SUPPRESSED_KEY] = set
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Event
|
|
5
|
+
module Trace
|
|
6
|
+
STACK_KEY = :_dex_event_trace_stack
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
include ExecutionState
|
|
10
|
+
|
|
11
|
+
def with_event(event, &block)
|
|
12
|
+
stack = _stack
|
|
13
|
+
stack.push(event.trace_frame)
|
|
14
|
+
yield
|
|
15
|
+
ensure
|
|
16
|
+
stack.pop
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def current_event_id
|
|
20
|
+
_stack.last&.dig(:id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current_trace_id
|
|
24
|
+
_stack.last&.dig(:trace_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dump
|
|
28
|
+
frame = _stack.last
|
|
29
|
+
return nil unless frame
|
|
30
|
+
|
|
31
|
+
{ id: frame[:id], trace_id: frame[:trace_id] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def restore(data, &block)
|
|
35
|
+
return yield unless data
|
|
36
|
+
|
|
37
|
+
stack = _stack
|
|
38
|
+
stack.push(data)
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
stack.pop if data
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear!
|
|
45
|
+
_execution_state[STACK_KEY] = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def _stack
|
|
51
|
+
_execution_state[STACK_KEY] ||= []
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/dex/event.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
# Modules loaded before class body (no reference to Dex::Event needed)
|
|
6
|
+
require_relative "event/execution_state"
|
|
7
|
+
require_relative "event/metadata"
|
|
8
|
+
require_relative "event/trace"
|
|
9
|
+
require_relative "event/suppression"
|
|
10
|
+
|
|
11
|
+
module Dex
|
|
12
|
+
class Event
|
|
13
|
+
RESERVED_PROP_NAMES = %i[
|
|
14
|
+
id timestamp trace_id caused_by_id caused_by
|
|
15
|
+
context publish metadata sync
|
|
16
|
+
].to_set.freeze
|
|
17
|
+
|
|
18
|
+
include PropsSetup
|
|
19
|
+
include TypeCoercion
|
|
20
|
+
|
|
21
|
+
def self._warn(message)
|
|
22
|
+
Dex.warn("Event: #{message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.validate_event_class!(klass)
|
|
26
|
+
return if klass.is_a?(Class) && klass < Dex::Event
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, "#{klass.inspect} is not a Dex::Event subclass"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# --- Instance ---
|
|
32
|
+
|
|
33
|
+
attr_reader :metadata
|
|
34
|
+
|
|
35
|
+
def after_initialize
|
|
36
|
+
@metadata = Metadata.build
|
|
37
|
+
freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Metadata delegates
|
|
41
|
+
def id = metadata.id
|
|
42
|
+
def timestamp = metadata.timestamp
|
|
43
|
+
def trace_id = metadata.trace_id
|
|
44
|
+
def caused_by_id = metadata.caused_by_id
|
|
45
|
+
def context = metadata.context
|
|
46
|
+
def trace_frame = { id: id, trace_id: trace_id }
|
|
47
|
+
|
|
48
|
+
# Publishing
|
|
49
|
+
def publish(sync: false)
|
|
50
|
+
Bus.publish(self, sync: sync)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.publish(sync: false, caused_by: nil, **kwargs)
|
|
54
|
+
if caused_by
|
|
55
|
+
Trace.with_event(caused_by) do
|
|
56
|
+
new(**kwargs).publish(sync: sync)
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
new(**kwargs).publish(sync: sync)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Tracing
|
|
64
|
+
def trace(&block)
|
|
65
|
+
Trace.with_event(self, &block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Suppression
|
|
69
|
+
def self.suppress(*classes, &block)
|
|
70
|
+
Suppression.suppress(*classes, &block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Serialization
|
|
74
|
+
def as_json
|
|
75
|
+
{
|
|
76
|
+
"type" => self.class.name,
|
|
77
|
+
"payload" => _props_as_json,
|
|
78
|
+
"metadata" => metadata.as_json
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Classes loaded after Event is defined (they reference Dex::Event)
|
|
85
|
+
require_relative "event/bus"
|
|
86
|
+
require_relative "event/handler"
|
|
87
|
+
require_relative "event/processor"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Event
|
|
5
|
+
module TestHelpers
|
|
6
|
+
def assert_event_published(event_class, msg: nil, **expected_props)
|
|
7
|
+
matching = _dex_find_published_events(event_class, expected_props)
|
|
8
|
+
assert matching.any?,
|
|
9
|
+
msg || _dex_event_published_failure_message(event_class, expected_props)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def refute_event_published(event_class = nil, msg: nil, **expected_props)
|
|
13
|
+
if event_class.nil?
|
|
14
|
+
assert _dex_published_events.empty?,
|
|
15
|
+
msg || "Expected no events published, but #{_dex_published_events.size} were:\n#{_dex_event_list}"
|
|
16
|
+
else
|
|
17
|
+
matching = _dex_find_published_events(event_class, expected_props)
|
|
18
|
+
assert matching.empty?,
|
|
19
|
+
msg || "Expected no #{event_class.name || event_class} events published, but found #{matching.size}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def assert_event_count(event_class, count, msg: nil)
|
|
24
|
+
matching = _dex_find_published_events(event_class, {})
|
|
25
|
+
assert_equal count, matching.size,
|
|
26
|
+
msg || "Expected #{count} #{event_class.name || event_class} events, got #{matching.size}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert_event_trace(parent, child, msg: nil)
|
|
30
|
+
assert_equal parent.id, child.caused_by_id,
|
|
31
|
+
msg || "Expected child event to be caused by parent (caused_by_id mismatch)"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def assert_same_trace(*events, msg: nil)
|
|
35
|
+
trace_ids = events.map(&:trace_id).uniq
|
|
36
|
+
assert_equal 1, trace_ids.size,
|
|
37
|
+
msg || "Expected all events to share the same trace_id, got: #{trace_ids.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def _dex_find_published_events(event_class, expected_props)
|
|
43
|
+
_dex_published_events.select do |event|
|
|
44
|
+
next false unless event.is_a?(event_class)
|
|
45
|
+
|
|
46
|
+
expected_props.all? do |key, value|
|
|
47
|
+
event.respond_to?(key) && event.public_send(key) == value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def _dex_event_published_failure_message(event_class, expected_props)
|
|
53
|
+
name = event_class.name || event_class.to_s
|
|
54
|
+
if _dex_published_events.empty?
|
|
55
|
+
"Expected #{name} to be published, but no events were published"
|
|
56
|
+
else
|
|
57
|
+
msg = "Expected #{name} to be published"
|
|
58
|
+
msg += " with #{expected_props.inspect}" unless expected_props.empty?
|
|
59
|
+
msg + ", but only found:\n#{_dex_event_list}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _dex_event_list
|
|
64
|
+
_dex_published_events.map.with_index do |event, i|
|
|
65
|
+
" #{i + 1}. #{event.class.name || event.class}"
|
|
66
|
+
end.join("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Event
|
|
5
|
+
module EventTestWrapper
|
|
6
|
+
CAPTURING_KEY = :_dex_event_capturing
|
|
7
|
+
PUBLISHED_KEY = :_dex_event_published
|
|
8
|
+
|
|
9
|
+
@_installed = false
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
include ExecutionState
|
|
13
|
+
|
|
14
|
+
def install!
|
|
15
|
+
return if @_installed
|
|
16
|
+
|
|
17
|
+
Dex::Event::Bus.singleton_class.prepend(BusInterceptor)
|
|
18
|
+
@_installed = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def installed?
|
|
22
|
+
@_installed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def capturing?
|
|
26
|
+
(_execution_state[CAPTURING_KEY] || 0) > 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def begin_capture!
|
|
30
|
+
_execution_state[CAPTURING_KEY] = (_execution_state[CAPTURING_KEY] || 0) + 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def end_capture!
|
|
34
|
+
depth = (_execution_state[CAPTURING_KEY] || 0) - 1
|
|
35
|
+
_execution_state[CAPTURING_KEY] = [depth, 0].max
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def published_events
|
|
39
|
+
_execution_state[PUBLISHED_KEY] ||= []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear_published!
|
|
43
|
+
_execution_state[PUBLISHED_KEY] = []
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module BusInterceptor
|
|
48
|
+
def publish(event, sync:)
|
|
49
|
+
if Dex::Event::EventTestWrapper.capturing?
|
|
50
|
+
return if Dex::Event::Suppression.suppressed?(event.class)
|
|
51
|
+
|
|
52
|
+
Dex::Event::EventTestWrapper.published_events << event
|
|
53
|
+
else
|
|
54
|
+
super(event, sync: true)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module TestHelpers
|
|
61
|
+
def self.included(base)
|
|
62
|
+
EventTestWrapper.install!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setup
|
|
66
|
+
super
|
|
67
|
+
EventTestWrapper.clear_published!
|
|
68
|
+
Dex::Event::Trace.clear!
|
|
69
|
+
Dex::Event::Suppression.clear!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def capture_events
|
|
73
|
+
EventTestWrapper.begin_capture!
|
|
74
|
+
yield
|
|
75
|
+
ensure
|
|
76
|
+
EventTestWrapper.end_capture!
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def _dex_published_events
|
|
82
|
+
EventTestWrapper.published_events
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
require_relative "event_test_helpers/assertions"
|
|
@@ -9,41 +9,41 @@ module Dex
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
|
-
|
|
13
|
-
if
|
|
14
|
-
|
|
12
|
+
ensure_active_job_loaded!
|
|
13
|
+
if use_record_strategy?
|
|
14
|
+
enqueue_record_job
|
|
15
15
|
else
|
|
16
|
-
|
|
16
|
+
enqueue_direct_job
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
job =
|
|
24
|
-
job.perform_later(class_name:
|
|
22
|
+
def enqueue_direct_job
|
|
23
|
+
job = apply_options(Operation::DirectJob)
|
|
24
|
+
job.perform_later(class_name: operation_class_name, params: serialized_params)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def enqueue_record_job
|
|
28
28
|
record = Dex.record_backend.create_record(
|
|
29
|
-
name:
|
|
30
|
-
params:
|
|
29
|
+
name: operation_class_name,
|
|
30
|
+
params: serialized_params,
|
|
31
31
|
status: "pending"
|
|
32
32
|
)
|
|
33
33
|
begin
|
|
34
|
-
job =
|
|
35
|
-
job.perform_later(class_name:
|
|
34
|
+
job = apply_options(Operation::RecordJob)
|
|
35
|
+
job.perform_later(class_name: operation_class_name, record_id: record.id)
|
|
36
36
|
rescue => e
|
|
37
37
|
begin
|
|
38
38
|
record.destroy
|
|
39
39
|
rescue => destroy_error
|
|
40
|
-
|
|
40
|
+
Dex.warn("Failed to clean up pending record #{record.id}: #{destroy_error.message}")
|
|
41
41
|
end
|
|
42
42
|
raise e
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def use_record_strategy?
|
|
47
47
|
return false unless Dex.record_backend
|
|
48
48
|
return false unless @operation.class.name
|
|
49
49
|
|
|
@@ -54,54 +54,48 @@ module Dex
|
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def apply_options(job_class)
|
|
58
58
|
options = {}
|
|
59
|
-
options[:queue] =
|
|
60
|
-
options[:wait_until] =
|
|
61
|
-
options[:wait] =
|
|
59
|
+
options[:queue] = queue if queue
|
|
60
|
+
options[:wait_until] = scheduled_at if scheduled_at
|
|
61
|
+
options[:wait] = scheduled_in if scheduled_in
|
|
62
62
|
options.empty? ? job_class : job_class.set(**options)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def ensure_active_job_loaded!
|
|
66
66
|
return if defined?(ActiveJob::Base)
|
|
67
67
|
|
|
68
68
|
raise LoadError, "ActiveJob is required for async operations. Add 'activejob' to your Gemfile."
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def merged_options
|
|
72
72
|
@operation.class.settings_for(:async).merge(@runtime_options)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
def
|
|
77
|
-
def
|
|
78
|
-
def
|
|
75
|
+
def queue = merged_options[:queue]
|
|
76
|
+
def scheduled_at = merged_options[:at]
|
|
77
|
+
def scheduled_in = merged_options[:in]
|
|
78
|
+
def operation_class_name = @operation.class.name
|
|
79
79
|
|
|
80
|
-
def
|
|
81
|
-
@
|
|
80
|
+
def serialized_params
|
|
81
|
+
@serialized_params ||= begin
|
|
82
82
|
hash = @operation._props_as_json
|
|
83
|
-
|
|
83
|
+
validate_serializable!(hash)
|
|
84
84
|
hash
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def
|
|
89
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
90
|
-
Rails.logger.warn "[Dex] #{message}"
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def _async_validate_serializable!(hash, path: "")
|
|
88
|
+
def validate_serializable!(hash, path: "")
|
|
95
89
|
hash.each do |key, value|
|
|
96
90
|
current = path.empty? ? key.to_s : "#{path}.#{key}"
|
|
97
91
|
case value
|
|
98
92
|
when String, Integer, Float, NilClass, TrueClass, FalseClass
|
|
99
93
|
next
|
|
100
94
|
when Hash
|
|
101
|
-
|
|
95
|
+
validate_serializable!(value, path: current)
|
|
102
96
|
when Array
|
|
103
97
|
value.each_with_index do |v, i|
|
|
104
|
-
|
|
98
|
+
validate_serializable!({ i => v }, path: current)
|
|
105
99
|
end
|
|
106
100
|
else
|
|
107
101
|
raise ArgumentError,
|
|
@@ -2,33 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Dex
|
|
4
4
|
module AsyncWrapper
|
|
5
|
-
ASYNC_KNOWN_OPTIONS = %i[queue in at].freeze
|
|
6
|
-
|
|
7
5
|
module ClassMethods
|
|
8
6
|
def async(**options)
|
|
9
|
-
|
|
10
|
-
if unknown.any?
|
|
11
|
-
raise ArgumentError,
|
|
12
|
-
"unknown async option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
13
|
-
"Known: #{AsyncWrapper::ASYNC_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
14
|
-
end
|
|
15
|
-
|
|
7
|
+
validate_options!(options, %i[queue in at], :async)
|
|
16
8
|
set(:async, **options)
|
|
17
9
|
end
|
|
18
10
|
end
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
base.extend(ClassMethods)
|
|
22
|
-
end
|
|
12
|
+
extend Dex::Concern
|
|
23
13
|
|
|
24
14
|
def async(**options)
|
|
25
|
-
|
|
26
|
-
if unknown.any?
|
|
27
|
-
raise ArgumentError,
|
|
28
|
-
"unknown async option(s): #{unknown.map(&:inspect).join(", ")}. " \
|
|
29
|
-
"Known: #{AsyncWrapper::ASYNC_KNOWN_OPTIONS.map(&:inspect).join(", ")}"
|
|
30
|
-
end
|
|
31
|
-
|
|
15
|
+
self.class.validate_options!(options, %i[queue in at], :async)
|
|
32
16
|
Operation::AsyncProxy.new(self, **options)
|
|
33
17
|
end
|
|
34
18
|
end
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Dex
|
|
4
4
|
module CallbackWrapper
|
|
5
|
-
|
|
6
|
-
base.extend(ClassMethods)
|
|
7
|
-
end
|
|
5
|
+
extend Dex::Concern
|
|
8
6
|
|
|
9
7
|
module ClassMethods
|
|
10
8
|
def before(callable = nil, &block)
|
|
@@ -57,24 +55,22 @@ module Dex
|
|
|
57
55
|
def _callback_wrap
|
|
58
56
|
return yield unless self.class._callback_any?
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
success_halt = nil
|
|
61
59
|
result = _callback_run_around(self.class._callback_list(:around)) do
|
|
62
60
|
_callback_run_before
|
|
63
|
-
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw(:_dex_halt, caught)
|
|
71
|
-
end
|
|
61
|
+
interceptor = Operation::HaltInterceptor.new { yield }
|
|
62
|
+
if interceptor.error?
|
|
63
|
+
interceptor.rethrow!
|
|
64
|
+
elsif interceptor.halted?
|
|
65
|
+
success_halt = interceptor
|
|
66
|
+
_callback_run_after
|
|
67
|
+
interceptor.result
|
|
72
68
|
else
|
|
73
69
|
_callback_run_after
|
|
74
|
-
|
|
70
|
+
interceptor.result
|
|
75
71
|
end
|
|
76
72
|
end
|
|
77
|
-
|
|
73
|
+
success_halt&.rethrow!
|
|
78
74
|
result
|
|
79
75
|
end
|
|
80
76
|
|
data/lib/dex/operation/jobs.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Dex
|
|
|
11
11
|
const_set(:DirectJob, Class.new(ActiveJob::Base) do
|
|
12
12
|
def perform(class_name:, params:)
|
|
13
13
|
klass = class_name.constantize
|
|
14
|
-
klass.new(**klass.send(:
|
|
14
|
+
klass.new(**klass.send(:_coerce_serialized_hash, params)).call
|
|
15
15
|
end
|
|
16
16
|
end)
|
|
17
17
|
when :RecordJob
|
|
@@ -19,39 +19,33 @@ module Dex
|
|
|
19
19
|
def perform(class_name:, record_id:)
|
|
20
20
|
klass = class_name.constantize
|
|
21
21
|
record = Dex.record_backend.find_record(record_id)
|
|
22
|
-
params = klass.send(:
|
|
22
|
+
params = klass.send(:_coerce_serialized_hash, record.params || {})
|
|
23
23
|
|
|
24
24
|
op = klass.new(**params)
|
|
25
25
|
op.instance_variable_set(:@_dex_record_id, record_id)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
update_status(record_id, status: "running")
|
|
28
28
|
op.call
|
|
29
29
|
rescue => e
|
|
30
|
-
|
|
30
|
+
handle_failure(record_id, e)
|
|
31
31
|
raise
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def update_status(record_id, **attributes)
|
|
37
37
|
Dex.record_backend.update_record(record_id, attributes)
|
|
38
38
|
rescue => e
|
|
39
|
-
|
|
39
|
+
Dex.warn("Failed to update record status: #{e.message}")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def
|
|
42
|
+
def handle_failure(record_id, exception)
|
|
43
43
|
error_value = if exception.is_a?(Dex::Error)
|
|
44
44
|
exception.code.to_s
|
|
45
45
|
else
|
|
46
46
|
exception.class.name
|
|
47
47
|
end
|
|
48
|
-
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def _dex_log_warning(message)
|
|
52
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
53
|
-
Rails.logger.warn "[Dex] #{message}"
|
|
54
|
-
end
|
|
48
|
+
update_status(record_id, status: "failed", error: error_value)
|
|
55
49
|
end
|
|
56
50
|
end)
|
|
57
51
|
when :Job
|