ductr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +14 -0
  4. data/.vscode/settings.json +18 -0
  5. data/COPYING +674 -0
  6. data/COPYING.LESSER +165 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +121 -0
  9. data/README.md +37 -0
  10. data/Rakefile +37 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/ductr.gemspec +50 -0
  14. data/exe/ductr +24 -0
  15. data/lib/ductr/adapter.rb +94 -0
  16. data/lib/ductr/cli/default.rb +25 -0
  17. data/lib/ductr/cli/main.rb +60 -0
  18. data/lib/ductr/cli/new_project_generator.rb +72 -0
  19. data/lib/ductr/cli/templates/project/bin_ductr.rb +7 -0
  20. data/lib/ductr/cli/templates/project/config_app.rb +5 -0
  21. data/lib/ductr/cli/templates/project/config_development.yml +8 -0
  22. data/lib/ductr/cli/templates/project/config_environment_development.rb +18 -0
  23. data/lib/ductr/cli/templates/project/gemfile.rb +6 -0
  24. data/lib/ductr/cli/templates/project/rubocop.yml +14 -0
  25. data/lib/ductr/cli/templates/project/tool-versions +1 -0
  26. data/lib/ductr/configuration.rb +145 -0
  27. data/lib/ductr/etl/controls/buffered_destination.rb +65 -0
  28. data/lib/ductr/etl/controls/buffered_transform.rb +76 -0
  29. data/lib/ductr/etl/controls/control.rb +46 -0
  30. data/lib/ductr/etl/controls/destination.rb +28 -0
  31. data/lib/ductr/etl/controls/paginated_source.rb +47 -0
  32. data/lib/ductr/etl/controls/source.rb +21 -0
  33. data/lib/ductr/etl/controls/transform.rb +28 -0
  34. data/lib/ductr/etl/fiber_control.rb +136 -0
  35. data/lib/ductr/etl/fiber_runner.rb +68 -0
  36. data/lib/ductr/etl/kiba_runner.rb +26 -0
  37. data/lib/ductr/etl/parser.rb +115 -0
  38. data/lib/ductr/etl/runner.rb +37 -0
  39. data/lib/ductr/etl_job.rb +161 -0
  40. data/lib/ductr/job.rb +58 -0
  41. data/lib/ductr/job_etl_runner.rb +37 -0
  42. data/lib/ductr/job_status.rb +56 -0
  43. data/lib/ductr/kiba_job.rb +130 -0
  44. data/lib/ductr/log/formatters/color_formatter.rb +48 -0
  45. data/lib/ductr/log/logger.rb +169 -0
  46. data/lib/ductr/log/outputs/file_output.rb +30 -0
  47. data/lib/ductr/log/outputs/standard_output.rb +39 -0
  48. data/lib/ductr/pipeline.rb +133 -0
  49. data/lib/ductr/pipeline_runner.rb +95 -0
  50. data/lib/ductr/pipeline_step.rb +92 -0
  51. data/lib/ductr/registry.rb +55 -0
  52. data/lib/ductr/rufus_trigger.rb +106 -0
  53. data/lib/ductr/scheduler.rb +117 -0
  54. data/lib/ductr/store/job_serializer.rb +59 -0
  55. data/lib/ductr/store/job_store.rb +59 -0
  56. data/lib/ductr/store/pipeline_serializer.rb +106 -0
  57. data/lib/ductr/store/pipeline_store.rb +48 -0
  58. data/lib/ductr/store.rb +81 -0
  59. data/lib/ductr/trigger.rb +49 -0
  60. data/lib/ductr/version.rb +6 -0
  61. data/lib/ductr.rb +143 -0
  62. data/sig/ductr.rbs +1107 -0
  63. metadata +292 -0
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module ETL
5
+ #
6
+ # Glues ETL controls and the associated fibers together.
7
+ #
8
+ class FiberControl
9
+ extend Forwardable
10
+
11
+ #
12
+ # @!method resume
13
+ # Resumes the control's fiber.
14
+ # @param [Object] row The row to pass to right fiber controls
15
+ # @return [void]
16
+ def_delegators :fiber, :resume
17
+
18
+ # @return [Array<FiberControl>] The next fiber controls
19
+ attr_accessor :right
20
+ # @return [Control] The ETL control instance
21
+ attr_reader :control
22
+
23
+ #
24
+ # Creates a new fiber control with the given control and control type.
25
+ #
26
+ # @param [Control] control The ETL control to work with in the fiber
27
+ # @param [Symbol] type The ETL control type, one of [:source, :transform, :destination]
28
+ #
29
+ def initialize(control, type:)
30
+ @control = control
31
+ @type = type
32
+
33
+ @right = []
34
+ end
35
+
36
+ #
37
+ # Memoizes the fiber to be associated with the ETL control based on its type.
38
+ #
39
+ # @return [Fiber] The fiber in charge of executing the control's logic
40
+ #
41
+ def fiber
42
+ @fiber ||= send(@type)
43
+ end
44
+
45
+ private
46
+
47
+ #
48
+ # Creates the fiber to run ETL sources.
49
+ #
50
+ # @return [Fiber]
51
+ #
52
+ def source
53
+ Fiber.new do
54
+ control.each do |row|
55
+ resume_right_fibers(row)
56
+ end
57
+
58
+ resume_right_fibers(:end)
59
+ end
60
+ end
61
+
62
+ #
63
+ # Creates the fiber to run ETL transforms.
64
+ #
65
+ # @return [Fiber]
66
+ #
67
+ def transform
68
+ resume_control(Fiber.new do
69
+ loop do
70
+ row_in = Fiber.yield
71
+ next close_transform if row_in == :end
72
+
73
+ row_out = control.process(row_in) do |r|
74
+ resume_right_fibers(r)
75
+ end
76
+
77
+ resume_right_fibers(row_out) if row_out
78
+ end
79
+ end)
80
+ end
81
+
82
+ #
83
+ # Creates the fiber to run ETL Destinations.
84
+ #
85
+ # @return [Fiber]
86
+ #
87
+ def destination
88
+ resume_control(Fiber.new do
89
+ loop do
90
+ row = Fiber.yield
91
+ next control.close if row == :end
92
+
93
+ control.write(row)
94
+ end
95
+ end)
96
+ end
97
+
98
+ #
99
+ # Call #close on control, resume resulting rows then ends following fibers.
100
+ #
101
+ # @return [void]
102
+ #
103
+ def close_transform
104
+ control.close do |row|
105
+ resume_right_fibers(row)
106
+ end
107
+ resume_right_fibers(:end)
108
+ end
109
+
110
+ #
111
+ # Resumes all fibers at the right of the current one.
112
+ #
113
+ # @param [Object] row The row to pass to the next fibers
114
+ #
115
+ # @return [void]
116
+ #
117
+ def resume_right_fibers(row)
118
+ right.each do |fiber|
119
+ fiber.resume(row)
120
+ end
121
+ end
122
+
123
+ #
124
+ # Resumes the given fiber and returns it.
125
+ #
126
+ # @param [Fiber] fiber The fiber to resume
127
+ #
128
+ # @return [Fiber] The resumed fiber
129
+ #
130
+ def resume_control(fiber)
131
+ fiber.resume
132
+ fiber
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module ETL
5
+ #
6
+ # A runner built with fibers. Compared to KibaRunner,
7
+ # this one allows to define how control are related to each other.
8
+ # These definitions can be found in Runner#pipes method.
9
+ #
10
+ class FiberRunner < Runner
11
+ #
12
+ # Initializes fibers and waits for them to finish.
13
+ #
14
+ # @return [void]
15
+ #
16
+ def run
17
+ create_fibers!
18
+ @source_fibers.each_value(&:resume)
19
+ end
20
+
21
+ private
22
+
23
+ #
24
+ # Initializes control fibers and pipes them together.
25
+ #
26
+ # @return [void]
27
+ #
28
+ def create_fibers!
29
+ @source_fibers = create_control_fibers(sources) { |s| FiberControl.new(s, type: :source) }
30
+ @transform_fibers = create_control_fibers(transforms) { |t| FiberControl.new(t, type: :transform) }
31
+ @destination_fibers = create_control_fibers(destinations) { |d| FiberControl.new(d, type: :destination) }
32
+
33
+ apply_fibers_plumbing!
34
+ end
35
+
36
+ #
37
+ # Pipes fiber controls together based on the control plumbing hash.
38
+ #
39
+ # @return [void]
40
+ #
41
+ def apply_fibers_plumbing!
42
+ pipes.map do |from_to|
43
+ from = from_to.keys.first
44
+ to = from_to[from]
45
+
46
+ input = { **@source_fibers, **@transform_fibers }[from]
47
+ outputs = to.map { |out| { **@transform_fibers, **@destination_fibers }[out] }
48
+
49
+ input.right = outputs
50
+ end
51
+ end
52
+
53
+ #
54
+ # Maps controls into a hash with job's method name as keys and control fibers as values.
55
+ #
56
+ # @param [Array<Control>] controls The controls to map on the hash
57
+ # @yield [control] The block in which the control fiber has to be initialized
58
+ #
59
+ # @return [Hash{Symbol => FiberControl}] The mapped hash
60
+ #
61
+ def create_control_fibers(controls, &)
62
+ controls.to_h do |control|
63
+ [control.job_method.name, yield(control)]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kiba"
4
+
5
+ module Ductr
6
+ module ETL
7
+ #
8
+ # A runner based on kiba's streaming runner
9
+ # @see Kiba's streaming runner source code to get details about its forwarded methods
10
+ #
11
+ class KibaRunner < Runner
12
+ extend Forwardable
13
+ def_delegators Kiba::StreamingRunner, :source_stream, :transform_stream, :process_rows, :close_destinations
14
+
15
+ #
16
+ # Calls kiba's streaming runner #process_rows and #close_destinations like Kiba::StreamingRunner#run
17
+ #
18
+ # @return [void]
19
+ #
20
+ def run
21
+ process_rows(sources, transforms, destinations)
22
+ close_destinations(destinations)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module ETL
5
+ #
6
+ # Contains anything to "parse" ETL jobs annotations.
7
+ # #parse_annotations handles ETL controls and send_to directives.
8
+ #
9
+ module Parser
10
+ #
11
+ # Handles sources, transforms and destinations controls.
12
+ # Handles send_to directives, used to do the plumbing between controls.
13
+ # Used for both kiba and fiber runners initialization.
14
+ #
15
+ # @return [Array<Source, Transform, Destination, Hash{Symbol => Array<Symbol>}>] The job's controls
16
+ #
17
+ def parse_annotations
18
+ sources = init_adapter_controls(:source)
19
+ transforms = init_transform_controls(:transform, :lookup)
20
+ destinations = init_adapter_controls(:destination)
21
+ pipes = find_method(:send_to) do |method|
22
+ { method.name => method.find_annotation(:send_to).params }
23
+ end
24
+
25
+ [sources, transforms, destinations, pipes]
26
+ end
27
+
28
+ #
29
+ # Currently used adapters set.
30
+ #
31
+ # @return [Set] The current adapters
32
+ #
33
+ def adapters
34
+ @adapters ||= Set.new
35
+ end
36
+
37
+ private
38
+
39
+ #
40
+ # Finds the method(s) associated to the given annotation names in the job class.
41
+ #
42
+ # @param [Array<Symbol>] *annotation_names The annotation names of the searched methods
43
+ # @yield [method] The block to execute on each founded methods
44
+ # @yieldparam [method] A job's method
45
+ #
46
+ # @return [Array] Returns mapped array containing the block's returned value
47
+ #
48
+ def find_method(*annotation_names, &)
49
+ self.class.annotated_methods(*annotation_names).map(&)
50
+ end
51
+
52
+ #
53
+ # Initializes adapter controls for the given type.
54
+ #
55
+ # @param [Symbol] control_type The adapter control type, one of :source or :destination
56
+ #
57
+ # @return [Array<Source, Destination>] The initialized adapter controls
58
+ #
59
+ def init_adapter_controls(control_type)
60
+ find_method(control_type) do |method|
61
+ adapter_control(method)
62
+ end
63
+ end
64
+
65
+ #
66
+ # Initializes transform controls for the given types.
67
+ #
68
+ # @param [Array<Symbol>] *control_types The transform control types, :transform and/or :lookup
69
+ #
70
+ # @return [Array<Transform>] The initialized transform controls
71
+ #
72
+ def init_transform_controls(*control_types)
73
+ find_method(*control_types) do |method|
74
+ next adapter_control(method) if method.annotation_exist?(:lookup)
75
+
76
+ transform_control(method)
77
+ end
78
+ end
79
+
80
+ #
81
+ # Initializes an adapter control (source, lookup or destination) based on the given annotated method.
82
+ #
83
+ # @param [Annotable::Method] annotated_method The control's method
84
+ #
85
+ # @return [Control] The adapter control instance
86
+ #
87
+ def adapter_control(annotated_method)
88
+ annotation = annotated_method.find_annotation(:source, :destination, :lookup)
89
+ adapter_name, control_type = annotation.params
90
+
91
+ adapter = Ductr.config.adapter(adapter_name)
92
+ control_class = adapter.class.send("#{annotation.name}_registry").find(control_type)
93
+ job_method = method(annotated_method.name)
94
+
95
+ adapters.add(adapter)
96
+ control_class.new(job_method, adapter, **annotation.options)
97
+ end
98
+
99
+ #
100
+ # Initializes a transform control.
101
+ #
102
+ # @param [Annotable::Method] annotated_method The transform's method
103
+ #
104
+ # @return [Transform] The transform control instance
105
+ #
106
+ def transform_control(annotated_method)
107
+ annotation = annotated_method.find_annotation(:transform)
108
+ transform_class = annotation.params.first || Transform
109
+ job_method = method(annotated_method.name)
110
+
111
+ transform_class.new(job_method, **annotation.options)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module ETL
5
+ #
6
+ # The base class for all runners
7
+ #
8
+ class Runner
9
+ # @return [Array<Source>] The runner source controls
10
+ attr_accessor :sources
11
+
12
+ # @return [Array<Transform>] The runner transform controls
13
+ attr_accessor :transforms
14
+
15
+ # @return [Array<Destination>] The runner destination controls
16
+ attr_accessor :destinations
17
+
18
+ # @return [Array<Hash{Symbol => Array<Symbol>}>] The controls plumbing hashes
19
+ attr_accessor :pipes
20
+
21
+ #
22
+ # Creates the runner instance.
23
+ #
24
+ # @param [Array<Source>] sources The job's source controls
25
+ # @param [Array<Transform>] transforms The job's transform controls
26
+ # @param [Array<Destination>] destinations The job's destination controls
27
+ # @param [Array<Hash{Symbol => Array<Symbol>}>] pipes The controls plumbing hashes
28
+ #
29
+ def initialize(sources, transforms, destinations, pipes = [])
30
+ @sources = sources
31
+ @transforms = transforms
32
+ @destinations = destinations
33
+ @pipes = pipes
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ #
5
+ # Base class for ETL job using the experimental fiber runner.
6
+ # Usage example:
7
+ #
8
+ # class MyETLJob < Ductr::ETLJob
9
+ # source :first_db, :basic
10
+ # send_to :the_transform, :the_other_transform
11
+ # def the_source(db)
12
+ # # ...
13
+ # end
14
+ #
15
+ # transform
16
+ # send_to :the_destination
17
+ # def the_transform(row)
18
+ # # ...
19
+ # end
20
+ #
21
+ # destination :first_db, :basic
22
+ # def the_destination(row, db)
23
+ # # ...
24
+ # end
25
+ #
26
+ # transform
27
+ # send_to :the_other_destination
28
+ # def the_other_transform(row)
29
+ # # ...
30
+ # end
31
+ #
32
+ # destination :second_db, :basic
33
+ # def the_other_destination(row, db)
34
+ # # ...
35
+ # end
36
+ # end
37
+ #
38
+ class ETLJob < Job
39
+ # @return [Class] The ETL runner class used by the job
40
+ ETL_RUNNER_CLASS = ETL::FiberRunner
41
+ include JobETLRunner
42
+
43
+ include ETL::Parser
44
+
45
+ #
46
+ # @!method self.source(adapter_name, source_type, **source_options)
47
+ # Annotation to define a source method
48
+ # @param adapter_name [Symbol] The adapter the source is running on
49
+ # @param source_type [Symbol] The type of source to run
50
+ # @param **source_options [Hash<Symbol: Object>] The options to pass to the source
51
+ #
52
+ # @example Source with Sequel SQLite adapter
53
+ # source :my_adapter, :paginated, page_size: 42
54
+ # def my_source(db, offset, limit)
55
+ # db[:items].offset(offset).limit(limit)
56
+ # end
57
+ #
58
+ # @see The chosen adapter documentation for further information on sources usage.
59
+ #
60
+ # @return [void]
61
+ #
62
+ annotable :source
63
+
64
+ #
65
+ # @!method self.transform(transform_class, **transform_options)
66
+ # Annotation to define a transform method
67
+ # @param transform_class [Class, nil] The class the transform is running on
68
+ # @param **transform_options [Hash<Symbol: Object>] The options to pass to the transform
69
+ #
70
+ # @example Transform without params
71
+ # transform
72
+ # def rename_keys(row)
73
+ # row[:new_name] = row.delete[:old_name]
74
+ # row[:new_email] = row.delete[:old_email]
75
+ # end
76
+ #
77
+ # @example Transform with params
78
+ # class RenameTransform < Ductr::ETL::Transform
79
+ # def process(row)
80
+ # call_method.each do |actual_name, new_name|
81
+ # new_key = "#{options[:prefix]}#{new_name}".to_sym
82
+ #
83
+ # row[new_key] = row.delete(actual_name)
84
+ # end
85
+ # end
86
+ # end
87
+ #
88
+ # transform RenameTransform, prefix: "some_"
89
+ # def rename
90
+ # { old_name: :new_name, old_email: :new_email }
91
+ # end
92
+ #
93
+ # @return [void]
94
+ #
95
+ annotable :transform
96
+
97
+ #
98
+ # @!method self.lookup(adapter_name, lookup_type, **lookup_options)
99
+ # Annotation to define a lookup method
100
+ # @param adapter_name [Symbol] The adapter the lookup is running on
101
+ # @param lookup_type [Symbol] The type of lookup to run
102
+ # @param **lookup_options [Hash<Symbol: Object>] The options to pass to the lookup
103
+ #
104
+ # @example Lookup with Sequel SQLite adapter
105
+ # lookup :my_other_adapter, :match, merge: [:id, :item], buffer_size: 4
106
+ # def joining_different_adapters(db, ids)
107
+ # db[:items_bis].select(:id, :item, :name).where(item: ids)
108
+ # end
109
+ #
110
+ # @see The chosen adapter documentation for further information on lookups usage.
111
+ #
112
+ # @return [void]
113
+ #
114
+ annotable :lookup
115
+
116
+ #
117
+ # @!method self.destination(adapter_name, destination_type, **destination_options)
118
+ # Annotation to define a destination method
119
+ # @param adapter_name [Symbol] The adapter the destination is running on
120
+ # @param destination_type [Symbol] The type of destination to run
121
+ # @param **destination_options [Hash<Symbol: Object>] The options to pass to the destination
122
+ #
123
+ # @example Destination with Sequel SQLite adapter
124
+ # destination :my_other_adapter, :basic
125
+ # def my_destination(row, db)
126
+ # db[:new_items].insert(name: row[:name], new_name: row[:new_name])
127
+ # end
128
+ #
129
+ # @see The chosen adapter documentation for further information on destinations usage.
130
+ #
131
+ # @return [void]
132
+ #
133
+ annotable :destination
134
+
135
+ #
136
+ # @!method self.send_to(*methods)
137
+ # Annotation to define which methods will follow the current one
138
+ # @param *methods [Array<Symbol>] The names of the following methods
139
+ #
140
+ # @example Source with Sequel SQLite adapter sending rows to two transforms
141
+ # source :my_adapter, :paginated, page_size: 42
142
+ # send_to :my_first_transform, :my_second_transform
143
+ # def my_source(db, offset, limit)
144
+ # db[:items].offset(offset).limit(limit)
145
+ # end
146
+ #
147
+ # transform
148
+ # def my_first_transform(row)
149
+ # # ...
150
+ # end
151
+ #
152
+ # transform
153
+ # def my_second_transform(row)
154
+ # # ...
155
+ # end
156
+ #
157
+ # @return [void]
158
+ #
159
+ annotable :send_to
160
+ end
161
+ end
data/lib/ductr/job.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ #
5
+ # The base class for any job, you can use it directly if you don't need an ETL job.
6
+ #
7
+ class Job < ActiveJob::Base
8
+ extend Annotable
9
+ extend Forwardable
10
+
11
+ include JobStatus
12
+
13
+ # @return [Exception] The occurred error if any
14
+ attr_reader :error
15
+ # @return [Symbol] The job's status, one of `:queued`, `:working`, `:completed` and `:failed`
16
+ attr_reader :status
17
+
18
+ queue_as :ductr_jobs
19
+
20
+ #
21
+ # The active job's perform method. DO NOT override it, implement the #run method instead.
22
+ #
23
+ # @return [void]
24
+ #
25
+ def perform(*_)
26
+ run
27
+ end
28
+
29
+ #
30
+ # The configured adapter instances.
31
+ #
32
+ # @param [Symbol] name The adapter name
33
+ #
34
+ # @return [Adapter] The adapter corresponding to the given name
35
+ #
36
+ def adapter(name)
37
+ Ductr.config.adapter(name)
38
+ end
39
+
40
+ #
41
+ # The job's logger instance.
42
+ #
43
+ # @return [Ductr::Log::Logger] The logger instance
44
+ #
45
+ def logger
46
+ @logger ||= Ductr.config.logging.new(self.class)
47
+ end
48
+
49
+ #
50
+ # The entry point of jobs.
51
+ #
52
+ # @return [void]
53
+ #
54
+ def run
55
+ raise NotImplementedError, "A job must implement the `#run` method"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ #
5
+ # Allowing a job to execute ETL runners.
6
+ # You need to declare the ETL_RUNNER_CLASS constant in the including class:
7
+ #
8
+ # class CustomJobClass < Job
9
+ # ETL_RUNNER_CLASS = ETL::KibaRunner
10
+ # include JobETLRunner
11
+ # end
12
+ #
13
+ # The job must have the #parse_annotations method defined, which can be added by including ETL::Parser.
14
+ #
15
+ module JobETLRunner
16
+ #
17
+ # Parse job's annotations and create the runner instance.
18
+ #
19
+ def initialize(...)
20
+ super(...)
21
+
22
+ @runner = self.class::ETL_RUNNER_CLASS.new(*parse_annotations)
23
+ end
24
+
25
+ #
26
+ # Opens adapters, executes the runner and then closes back adapters.
27
+ #
28
+ # @return [void]
29
+ #
30
+ def run
31
+ adapters.each(&:open!)
32
+ @runner.run
33
+ ensure
34
+ adapters.each(&:close!)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotable"
4
+
5
+ module Ductr
6
+ #
7
+ # This module contains the job's status tracking logic.
8
+ # It relies on Active Job's callbacks to write status into the store.
9
+ #
10
+ module JobStatus
11
+ class << self
12
+ #
13
+ # Registers the ActiveJob's `before_enqueue`, `before_perform` and `after_perform` callbacks
14
+ # to write status in the Ductr's store.
15
+ # Intercepts and re-raises job's exceptions to write the `:failed` status.
16
+ #
17
+ # @param [Class<Job>] job_class The job's class
18
+ #
19
+ # @return [void]
20
+ #
21
+ def included(job_class)
22
+ job_class.before_enqueue { |job| job.status = :queued }
23
+ job_class.before_perform { |job| job.status = :working }
24
+ job_class.after_perform { |job| job.status = :completed }
25
+
26
+ job_class.rescue_from(Exception) do |e|
27
+ @error = e
28
+ self.status = :failed
29
+
30
+ raise e
31
+ end
32
+ end
33
+ end
34
+
35
+ #
36
+ # Writes the job's status into the Ductr's store.
37
+ #
38
+ # @param [Symbol] status The status of the job
39
+ #
40
+ # @return [void]
41
+ #
42
+ def status=(status)
43
+ @status = status
44
+ Store.write_job(self)
45
+ end
46
+
47
+ #
48
+ # Determines whether the job has a `completed` or `failed` status.
49
+ #
50
+ # @return [Boolean] True when the status is `completed` or `failed`
51
+ #
52
+ def stopped?
53
+ %i[completed failed].include? status
54
+ end
55
+ end
56
+ end