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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ #
5
+ # Representation of a pipeline's step.
6
+ # Hold a fiber to execute steps concurrently.
7
+ #
8
+ class PipelineStep
9
+ extend Forwardable
10
+
11
+ #
12
+ # @!method resume
13
+ # Resumes the step's fiber.
14
+ # @return [void]
15
+ # @!method alive?
16
+ # Check if the step's fiber is running.
17
+ # @return [Boolean] True if step's fiber is running
18
+ #
19
+ def_delegators :fiber, :resume, :alive?
20
+
21
+ # @return [Pipeline] The step's pipeline
22
+ attr_reader :pipeline
23
+ # @return [Symbol] The step's name
24
+ attr_reader :name
25
+ # @return [Array<Job>] The step's queued jobs
26
+ attr_reader :jobs
27
+
28
+ # @return [PipelineStep] The previous step
29
+ attr_accessor :left
30
+
31
+ #
32
+ # Creates a step for the given pipeline.
33
+ #
34
+ # @param [Pipeline] pipeline The pipeline containing step's method
35
+ # @param [Symbol] The name of the pipeline's step method
36
+ #
37
+ def initialize(pipeline, name)
38
+ @pipeline = pipeline
39
+ @name = name
40
+
41
+ @jobs = []
42
+ @left = []
43
+ end
44
+
45
+ #
46
+ # Track, registers and enqueues the given job.
47
+ #
48
+ # @param [Job] job The job to enqueue
49
+ #
50
+ # @return [void]
51
+ #
52
+ def enqueue_job(job)
53
+ jobs.push(job)
54
+ Store.register_job(job)
55
+ job.enqueue
56
+ end
57
+
58
+ #
59
+ # Check if the step is done.
60
+ #
61
+ # @return [Boolean] True if the step is done
62
+ #
63
+ def done?
64
+ !fiber.alive?
65
+ end
66
+
67
+ #
68
+ # Waits until all step's jobs are stopped.
69
+ #
70
+ # @return [void]
71
+ #
72
+ def flush_jobs
73
+ return if jobs.empty?
74
+
75
+ Fiber.yield until Store.read_jobs(*jobs).all?(&:stopped?)
76
+ end
77
+
78
+ #
79
+ # The step's fiber instance, invokes the pipeline's step method.
80
+ #
81
+ # @return [Fiber] The step's fiber
82
+ #
83
+ def fiber
84
+ @fiber ||= Fiber.new do
85
+ Fiber.yield until left.all?(&:done?)
86
+
87
+ pipeline.send(name)
88
+ flush_jobs
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ class NotFoundInRegistryError < StandardError; end
5
+
6
+ #
7
+ # The registry pattern to store adapters, controls and triggers.
8
+ #
9
+ class Registry
10
+ extend Forwardable
11
+
12
+ #
13
+ # @!method values
14
+ # Get all registered adapters, controls or triggers.
15
+ # @return [Array<Class<Adapter, ETL::Control, Trigger>>] The registered adapters, controls or triggers
16
+ #
17
+ def_delegators :@items, :values
18
+
19
+ #
20
+ # Initialize the registry as an empty hash with the given name.
21
+ #
22
+ # @param name [Symbol] The registry name, used in error message
23
+ #
24
+ def initialize(name)
25
+ @name = name
26
+ @items = {}
27
+ end
28
+
29
+ #
30
+ # Allow to add one class into the registry.
31
+ #
32
+ # @param item [Class<Adapter, ETL::Control, Trigger>] The class to add in the registry
33
+ # @param as: [Symbol] The adapter, control or trigger type
34
+ #
35
+ # @return [void]
36
+ #
37
+ def add(item, as:)
38
+ @items[as.to_sym] = item
39
+ end
40
+
41
+ #
42
+ # Find an adapter, control or trigger based on its type.
43
+ #
44
+ # @param type [Symbol] The adapter, control or trigger type
45
+ #
46
+ # @raise [NotFoundInRegistryError] If nothing matches the given type
47
+ # @return [Class<Adapter, ETL::Control, Trigger>] The found adapter
48
+ #
49
+ def find(type)
50
+ @items.fetch(type.to_sym) do
51
+ raise NotFoundInRegistryError, "The #{@name} of type \"#{type}\" does not exist"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rufus-scheduler"
4
+
5
+ module Ductr
6
+ #
7
+ # A time related trigger, used to trigger jobs or pipelines based on temporal events.
8
+ # The trigger is registered as `:schedule`:
9
+ #
10
+ # trigger :schedule, every: "1min"
11
+ # def every_minute
12
+ # MyPipeline.perform_later
13
+ # end
14
+ #
15
+ # This trigger is based on the `rufus-scheduler` gem.
16
+ # Under the hood, the internal method `Rufus::Scheduler#do_schedule` is used.
17
+ #
18
+ # There are 4 types of options available:
19
+ #
20
+ # - `:once`, used to schedule at a given time
21
+ #
22
+ # trigger :schedule, once: "10d" # Will run in ten days
23
+ # trigger :schedule, once: "2042/12/12 23:59:00" # Will run at the given date
24
+ #
25
+ # - `:every`, triggers following the given frequency
26
+ #
27
+ # trigger :schedule, every: "1min" # will run every minute.
28
+ #
29
+ # - `:interval`, run the trigger then waits the given interval before running again
30
+ #
31
+ # # Will run every 4 + 1 seconds
32
+ # trigger :schedule, interval: "4s"
33
+ # def every_interval
34
+ # sleep(1)
35
+ # MyPipeline.perform_later
36
+ # end
37
+ #
38
+ # - `:cron`, run the trigger following the given crontab pattern
39
+ #
40
+ # trigger :schedule, cron: "00 01 * * *" # Will run every day at 1am.
41
+ #
42
+ class RufusTrigger < Trigger
43
+ Ductr.trigger_registry.add self, as: :schedule
44
+
45
+ #
46
+ # Adds a new trigger into rufus-scheduler.
47
+ #
48
+ # @param [Symbol] method The scheduler method to be called by rufus-scheduler
49
+ # @param [Hash<Symbol: String>] options The options to configure rufus-scheduler
50
+ #
51
+ # @return [void]
52
+ #
53
+ def add(method, options)
54
+ rufus_type = options.keys.first
55
+ rufus_value = options.values.first
56
+
57
+ do_schedule(rufus_type, rufus_value, method)
58
+ end
59
+
60
+ #
61
+ # Shutdown rufus-scheduler
62
+ #
63
+ # @return [void]
64
+ #
65
+ def stop
66
+ rufus.shutdown
67
+ end
68
+
69
+ private
70
+
71
+ #
72
+ # Shortcut to get the rufus-scheduler singleton
73
+ #
74
+ # @return [Rufus::Scheduler] The rufus-scheduler instance
75
+ #
76
+ def rufus
77
+ Rufus::Scheduler.singleton
78
+ end
79
+
80
+ #
81
+ # Returns a callable object based on given scheduler, method_name and options.
82
+ #
83
+ # @param [Scheduler] scheduler The scheduler instance
84
+ # @param [Symbol] method_name The scheduler's method name
85
+ # @param [Hash] ** The option passed to the trigger annotation
86
+ #
87
+ # @return [#call] A callable object
88
+ #
89
+ def callable(scheduler, method_name, **)
90
+ scheduler.method(method_name)
91
+ end
92
+
93
+ #
94
+ # Calls the Rufus::Scheduler#do_schedule private method with rufus-scheduler type, value and the callable object.
95
+ #
96
+ # @param [Symbol] rufus_type The rufus-scheduler type (`:once`, `:every`, `:interval` or `:cron`)
97
+ # @param [String] rufus_value The rufus-scheduler value (e.g. `"10min"`)
98
+ # @param [#call] method The callable object (the scheduler's method in this case)
99
+ #
100
+ # @return [void]
101
+ #
102
+ def do_schedule(rufus_type, rufus_value, method)
103
+ rufus.send(:do_schedule, rufus_type, rufus_value, nil, {}, false, method)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ #
5
+ # Base class to declare event driven scheduling.
6
+ # Example using the schedule trigger:
7
+ #
8
+ # class MyScheduler < Ductr::Scheduler
9
+ # trigger :schedule, every: "10min"
10
+ # def every_ten_minutes
11
+ # MyJob.perform_later
12
+ # end
13
+ # end
14
+ #
15
+ class Scheduler
16
+ extend Annotable
17
+
18
+ #
19
+ # @!method self.trigger(trigger_type, adapter_name = nil, **trigger_options)
20
+ # Annotation to define a trigger method
21
+ # @param trigger_type [Symbol] The trigger's type
22
+ # @param adapter_name [Symbol] The trigger's adapter (if any)
23
+ # @param **trigger_options [Hash<Symbol: Object>] The options to pass to the trigger
24
+ #
25
+ # @example A schedule trigger
26
+ # trigger :schedule, every: "10min"
27
+ # def every_ten_minutes
28
+ # MyJob.perform_later
29
+ # end
30
+ #
31
+ # @see The chosen trigger documentation for further information.
32
+ #
33
+ # @return [void]
34
+ #
35
+ annotable :trigger
36
+
37
+ class << self
38
+ #
39
+ # All trigger instances are stored in a singleton hash, avoiding to create the same trigger multiple times.
40
+ #
41
+ # @return [Hash<Symbol: Trigger>] The singleton hash containing all trigger instances
42
+ #
43
+ def triggers
44
+ @triggers ||= {}
45
+ end
46
+
47
+ #
48
+ # Calls #start on all created triggers.
49
+ #
50
+ # @return [void]
51
+ #
52
+ def start
53
+ triggers.values.each(&:start)
54
+ end
55
+
56
+ #
57
+ # Calls #stop on all created triggers.
58
+ #
59
+ # @return [void]
60
+ #
61
+ def stop
62
+ triggers.values.each(&:stop)
63
+ end
64
+ end
65
+
66
+ #
67
+ # Parses trigger annotations, creates triggers if needed and calls #add on trigger instances.
68
+ #
69
+ def initialize
70
+ self.class.annotated_methods.each do |method|
71
+ annotation = method.find_annotation(:trigger)
72
+ trigger = find_trigger(*annotation.params.reverse)
73
+ callable = self.method(method.name)
74
+
75
+ trigger.add(callable, annotation.options)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ #
82
+ # Finds a trigger in the according trigger registry based on its type and adapter name if given.
83
+ #
84
+ # @param [Symbol] trigger_type The trigger type, e.g. `:schedule`
85
+ # @param [Symbol] adapter_name The adapter name, e.g. `:my_adapter`
86
+ #
87
+ # @return [Trigger] The found trigger
88
+ #
89
+ def find_trigger(trigger_type, adapter_name = nil)
90
+ return find_or_create_trigger(Ductr.trigger_registry, trigger_type) unless adapter_name
91
+
92
+ adapter = Ductr.config.adapter(adapter_name)
93
+ trigger_registry = adapter.class.trigger_registry
94
+ registry_key = "#{adapter_name}_#{trigger_type}".to_sym
95
+
96
+ find_or_create_trigger(trigger_registry, registry_key, trigger_type, adapter)
97
+ end
98
+
99
+ #
100
+ # Find a trigger in the singleton hash. if none found, find the trigger class in the given registry,
101
+ # initializes it and store the instance in the singleton hash.
102
+ #
103
+ # @param [Registry] trigger_registry The registry containing the required trigger class
104
+ # @param [Symbol] registry_key The key in which to store the trigger instance inside the singleton hash
105
+ # @param [Symbol] trigger_type The registry's key in which to find the trigger class
106
+ # @param [Adapter, Nil] adapter The adapter to pass to the trigger
107
+ #
108
+ # @return [Trigger] The found or created trigger
109
+ #
110
+ def find_or_create_trigger(trigger_registry, registry_key, trigger_type = registry_key, adapter = nil)
111
+ trigger = Scheduler.triggers[registry_key]
112
+ return trigger if trigger
113
+
114
+ Scheduler.triggers[registry_key] = trigger_registry.find(trigger_type).new(adapter)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module Store
5
+ #
6
+ # Convert jobs into active job serializable structs.
7
+ #
8
+ module JobSerializer
9
+ #
10
+ # @!parse
11
+ # #
12
+ # # The job representation as a struct.
13
+ # #
14
+ # # @!attribute [r] job_id
15
+ # # @return [String] The active job's job id
16
+ # #
17
+ # # @!attribute [r] status
18
+ # # @return [Symbol] The job's status
19
+ # #
20
+ # # @!attribute [r] error
21
+ # # @return [Exception, nil] The job's error if any
22
+ # #
23
+ # class SerializedJob < Struct
24
+ # #
25
+ # # @param [String] job_id Active job's job id
26
+ # # @param [Symbol] status Job's status
27
+ # # @param [Exception, nil] error Job's error
28
+ # #
29
+ # def initialize(job_id, status, error)
30
+ # @job_id = job_id
31
+ # @status = status
32
+ # @error = error
33
+ # end
34
+ # end
35
+ #
36
+ SerializedJob = Struct.new(:job_id, :status, :error) do
37
+ #
38
+ # Determines whether the job has a `completed` or `failed` status.
39
+ #
40
+ # @return [Boolean] True when the status is `completed` or `failed`
41
+ #
42
+ def stopped?
43
+ %i[completed failed].include? status
44
+ end
45
+ end
46
+
47
+ #
48
+ # Convert the given job into a `SerializedJob` struct.
49
+ #
50
+ # @param [Job] job The job to serialize
51
+ #
52
+ # @return [SerializedJob] The job converted into struct
53
+ #
54
+ def serialize_job(job)
55
+ SerializedJob.new(job.job_id, job.status, job.error)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module Store
5
+ #
6
+ # Job's level store interactions.
7
+ #
8
+ module JobStore
9
+ include JobSerializer
10
+
11
+ # @return [String] The job key prefix
12
+ JOB_KEY_PREFIX = "ductr:job"
13
+ # @return [String] The job registry key
14
+ JOB_REGISTRY_KEY = "ductr:job_registry"
15
+
16
+ #
17
+ # Get all known job instances.
18
+ #
19
+ # @return [Array<Job>] The job instances
20
+ #
21
+ def all_jobs
22
+ all(JOB_REGISTRY_KEY, JOB_KEY_PREFIX)
23
+ end
24
+
25
+ #
26
+ # Read all given jobs.
27
+ #
28
+ # @param [Array<Job>] *jobs The jobs to read
29
+ #
30
+ # @return [Array<Job>] The read jobs
31
+ #
32
+ def read_jobs(*jobs)
33
+ read(JOB_KEY_PREFIX, *jobs)
34
+ end
35
+
36
+ #
37
+ # Update the given job.
38
+ #
39
+ # @param [Job] job The job to update in the store
40
+ #
41
+ # @return [void]
42
+ #
43
+ def write_job(job)
44
+ write(JOB_KEY_PREFIX, serialize_job(job))
45
+ end
46
+
47
+ #
48
+ # Add the given job to the store's job registry. This method is NOT thread-safe.
49
+ #
50
+ # @param [Job] job The job to register
51
+ #
52
+ # @return [void]
53
+ #
54
+ def register_job(job)
55
+ register(JOB_REGISTRY_KEY, serialize_job(job))
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module Store
5
+ #
6
+ # Convert pipelines and steps into active job serializable structs.
7
+ #
8
+ module PipelineSerializer
9
+ include JobSerializer
10
+
11
+ #
12
+ # @!parse
13
+ # #
14
+ # # The pipeline representation as a struct.
15
+ # #
16
+ # # @!attribute [r] job_id
17
+ # # @return [String] The active job's job id
18
+ # #
19
+ # # @!attribute [r] status
20
+ # # @return [Symbol] The pipeline job status
21
+ # #
22
+ # # @!attribute [r] error
23
+ # # @return [Exception, nil] The pipeline job error if any
24
+ # #
25
+ # # @!attribute [r] steps
26
+ # # @return [Array<SerializedPipelineStep>] The pipeline steps as struct
27
+ # #
28
+ # class SerializedPipeline < Struct
29
+ # #
30
+ # # @param [String] job_id Pipeline job id
31
+ # # @param [Symbol] status Pipeline status
32
+ # # @param [Exception, nil] error Pipeline error
33
+ # # @param [Array<SerializedPipelineStep>] steps Pipeline steps as struct
34
+ # #
35
+ # def initialize(job_id, status, error, steps)
36
+ # @job_id = job_id
37
+ # @status = status
38
+ # @error = error
39
+ # @steps = steps
40
+ # end
41
+ # end
42
+ #
43
+ SerializedPipeline = Struct.new(:job_id, :status, :error, :steps) do
44
+ #
45
+ # Determines whether the pipeline has a `completed` or `failed` status.
46
+ #
47
+ # @return [Boolean] True when the status is `completed` or `failed`
48
+ #
49
+ def stopped?
50
+ %i[completed failed].include? status
51
+ end
52
+ end
53
+
54
+ #
55
+ # @!parse
56
+ # #
57
+ # # The pipeline step representation as a struct.
58
+ # #
59
+ # # @!attribute [r] jobs
60
+ # # @return [Array<Job>] The step's jobs
61
+ # #
62
+ # # @!attribute [r] done
63
+ # # @return [Boolean] The step's fiber state
64
+ # #
65
+ # class SerializedPipelineStep < Struct
66
+ # #
67
+ # # @param [Array<Job>] jobs The step's jobs
68
+ # # @param [Boolean] done The step's fiber state
69
+ # #
70
+ # def initialize(jobs, done)
71
+ # @jobs = jobs
72
+ # @done = done
73
+ # end
74
+ # end
75
+ #
76
+ SerializedPipelineStep = Struct.new(:jobs, :done) do
77
+ #
78
+ # Check if the step is done.
79
+ #
80
+ # @return [Boolean] True if the step is done
81
+ #
82
+ def done?
83
+ done
84
+ end
85
+ end
86
+
87
+ #
88
+ # Convert the given pipeline and its steps into
89
+ # `SerializedPipeline` and `SerializedPipelineStep` structs.
90
+ #
91
+ # @param [Pipeline] pipeline The pipeline to serialize
92
+ #
93
+ # @return [SerializedPipeline] The pipeline converted into struct
94
+ #
95
+ def serialize_pipeline(pipeline)
96
+ serialized_steps = pipeline.runner.steps.map do |step|
97
+ jobs = step.jobs.map { |j| serialize_job(j) }
98
+
99
+ SerializedPipelineStep.new(jobs, step.done?)
100
+ end
101
+
102
+ SerializedPipeline.new(pipeline.job_id, pipeline.status, pipeline.error, serialized_steps)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductr
4
+ module Store
5
+ #
6
+ # Pipeline's level store interactions.
7
+ #
8
+ module PipelineStore
9
+ include PipelineSerializer
10
+
11
+ # @return [String] The pipeline key prefix
12
+ PIPELINE_KEY_PREFIX = "ductr:pipeline"
13
+ # @return [String] The pipeline registry key
14
+ PIPELINE_REGISTRY_KEY = "ductr:pipeline_registry"
15
+
16
+ #
17
+ # Get all known pipeline instances.
18
+ #
19
+ # @return [Array<SerializedPipeline>] The pipeline instances
20
+ #
21
+ def all_pipelines
22
+ all(PIPELINE_REGISTRY_KEY, PIPELINE_KEY_PREFIX)
23
+ end
24
+
25
+ #
26
+ # Update the given pipeline.
27
+ #
28
+ # @param [Pipeline] pipeline The pipeline to update in the store
29
+ #
30
+ # @return [void]
31
+ #
32
+ def write_pipeline(pipeline)
33
+ write(PIPELINE_KEY_PREFIX, serialize_pipeline(pipeline))
34
+ end
35
+
36
+ #
37
+ # Add the given pipeline to the store's pipeline registry. This method is NOT thread-safe.
38
+ #
39
+ # @param [Pipeline] pipeline The job to register
40
+ #
41
+ # @return [void]
42
+ #
43
+ def register_pipeline(pipeline)
44
+ register(PIPELINE_REGISTRY_KEY, pipeline)
45
+ end
46
+ end
47
+ end
48
+ end