qyu 1.0.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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +56 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE +21 -0
  8. data/README.md +90 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/server +17 -0
  12. data/bin/setup +8 -0
  13. data/examples/bin/simple +7 -0
  14. data/examples/config.rb +22 -0
  15. data/examples/simple/create_workflow.rb +18 -0
  16. data/examples/simple/enqueue_job.rb +8 -0
  17. data/examples/simple/worker.rb +32 -0
  18. data/lib/qyu.rb +74 -0
  19. data/lib/qyu/config.rb +35 -0
  20. data/lib/qyu/errors.rb +4 -0
  21. data/lib/qyu/errors/base.rb +8 -0
  22. data/lib/qyu/errors/could_not_fetch_task.rb +18 -0
  23. data/lib/qyu/errors/invalid_queue_name.rb +12 -0
  24. data/lib/qyu/errors/invalid_task_attributes.rb +12 -0
  25. data/lib/qyu/errors/job_not_found.rb +14 -0
  26. data/lib/qyu/errors/lock_already_acquired.rb +12 -0
  27. data/lib/qyu/errors/lock_not_acquired.rb +12 -0
  28. data/lib/qyu/errors/message_not_received.rb +12 -0
  29. data/lib/qyu/errors/not_implemented_error.rb +12 -0
  30. data/lib/qyu/errors/payload_validation_error.rb +12 -0
  31. data/lib/qyu/errors/task_not_found.rb +15 -0
  32. data/lib/qyu/errors/task_status_update_failed.rb +15 -0
  33. data/lib/qyu/errors/unknown_validation_option.rb +12 -0
  34. data/lib/qyu/errors/unsync_error.rb +12 -0
  35. data/lib/qyu/errors/workflow_descriptor_validation_error.rb +14 -0
  36. data/lib/qyu/errors/workflow_not_found.rb +15 -0
  37. data/lib/qyu/factory.rb +26 -0
  38. data/lib/qyu/models.rb +9 -0
  39. data/lib/qyu/models/concerns/workflow_descriptor_validator.rb +117 -0
  40. data/lib/qyu/models/enums/status.rb +44 -0
  41. data/lib/qyu/models/job.rb +174 -0
  42. data/lib/qyu/models/task.rb +218 -0
  43. data/lib/qyu/models/workflow.rb +85 -0
  44. data/lib/qyu/queue.rb +5 -0
  45. data/lib/qyu/queue/base.rb +46 -0
  46. data/lib/qyu/queue/memory/adapter.rb +90 -0
  47. data/lib/qyu/store.rb +5 -0
  48. data/lib/qyu/store/base.rb +106 -0
  49. data/lib/qyu/store/memory/adapter.rb +187 -0
  50. data/lib/qyu/ui.rb +56 -0
  51. data/lib/qyu/ui/helpers/pagination.rb +35 -0
  52. data/lib/qyu/ui/public/bootstrap.min.css +5 -0
  53. data/lib/qyu/ui/public/paper-dashboard.css +3315 -0
  54. data/lib/qyu/ui/public/script.js +28 -0
  55. data/lib/qyu/ui/public/style.css +6 -0
  56. data/lib/qyu/ui/views/footer.erb +18 -0
  57. data/lib/qyu/ui/views/helpers/pagination.erb +49 -0
  58. data/lib/qyu/ui/views/jobs.erb +58 -0
  59. data/lib/qyu/ui/views/kaminari/_first_page.html.erb +3 -0
  60. data/lib/qyu/ui/views/kaminari/_gap.html.erb +3 -0
  61. data/lib/qyu/ui/views/kaminari/_last_page.html.erb +3 -0
  62. data/lib/qyu/ui/views/kaminari/_next_page.html.erb +3 -0
  63. data/lib/qyu/ui/views/kaminari/_page.html.erb +9 -0
  64. data/lib/qyu/ui/views/kaminari/_paginator.html.erb +15 -0
  65. data/lib/qyu/ui/views/kaminari/_prev_page.html.erb +3 -0
  66. data/lib/qyu/ui/views/layout.erb +33 -0
  67. data/lib/qyu/ui/views/navbar.erb +29 -0
  68. data/lib/qyu/ui/views/pagination.erb +19 -0
  69. data/lib/qyu/ui/views/show_job.erb +55 -0
  70. data/lib/qyu/ui/views/sidebar.erb +17 -0
  71. data/lib/qyu/ui/views/task_row.erb +26 -0
  72. data/lib/qyu/utils.rb +17 -0
  73. data/lib/qyu/version.rb +3 -0
  74. data/lib/qyu/workers.rb +10 -0
  75. data/lib/qyu/workers/base.rb +126 -0
  76. data/lib/qyu/workers/concerns/callback.rb +38 -0
  77. data/lib/qyu/workers/concerns/failure_queue.rb +23 -0
  78. data/lib/qyu/workers/concerns/payload_validator.rb +124 -0
  79. data/lib/qyu/workers/sync.rb +63 -0
  80. data/qyu.gemspec +36 -0
  81. metadata +278 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ # Qyu::Config
5
+ class Config
6
+ attr_reader :queue, :store
7
+
8
+ class ServiceConfig
9
+ class << self
10
+ def register(adapter_class)
11
+ types[adapter_class::TYPE] = adapter_class
12
+ end
13
+
14
+ def valid?(config)
15
+ types[config[:type]].valid_config?(config)
16
+ end
17
+
18
+ def types
19
+ @__types ||= {}
20
+ end
21
+ end
22
+ end
23
+
24
+ class QueueConfig < ServiceConfig; end
25
+ class StoreConfig < ServiceConfig; end
26
+
27
+ def initialize(queue:, store:)
28
+ fail 'Invalid message queue configuration' unless QueueConfig.valid?(queue)
29
+ fail 'Invalid state store configuration' unless StoreConfig.valid?(store)
30
+
31
+ @queue = queue
32
+ @store = store
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ require 'qyu/errors/base'
2
+ (Dir["#{File.dirname(__FILE__)}/errors/*.rb"]).each do |path|
3
+ require path
4
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::Base
6
+ class Base < ::StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::CouldNotFetchTask
6
+ # TODO: rethink this...
7
+ class CouldNotFetchTask < Base
8
+ attr_reader :original_error, :queue_name, :message_id, :task_id
9
+ def initialize(queue_name, message_id, task_id, original_error)
10
+ super("Task cannot be fetched from queue=#{queue_name} with message_id=#{message_id} task_id=#{task_id}.")
11
+ @original_error = original_error
12
+ @task_id = task_id
13
+ @message_id = message_id
14
+ @queue_name = queue_name
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::InvalidQueueName
6
+ class InvalidQueueName < Base
7
+ def initialize
8
+ super('Queue name is invalid.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::InvalidTaskAttributes
6
+ class InvalidTaskAttributes < Base
7
+ def initialize
8
+ super('Invalid task attributes.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::JobNotFound
6
+ class JobNotFound < Base
7
+ attr_reader :original_error
8
+ def initialize(id, original_error)
9
+ super("Job not found with id=#{id}.")
10
+ @original_error = original_error
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::LockAlreadyAcquired
6
+ class LockAlreadyAcquired < Base
7
+ def initialize
8
+ super('Lock already acquired.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::LockNotAcquired
6
+ class LockNotAcquired < Base
7
+ def initialize
8
+ super('Lock was not acquired.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::MessageNotReceived
6
+ class MessageNotReceived < Base
7
+ def initialize
8
+ super('No message retrieved for task from queue.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::NotImplementedError
6
+ class NotImplementedError < Base
7
+ def initialize
8
+ super('Abstract method. Should have been overwritten.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::PayloadValidationError
6
+ class PayloadValidationError < Base
7
+ def initialize(validation_errors_hash)
8
+ super("Validation failed for payload fields: #{validation_errors_hash}.")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::TaskNotFound
6
+ class TaskNotFound < Base
7
+ attr_reader :task_id, :original_error
8
+ def initialize(task_id, original_error)
9
+ super("Task not found with id=#{task_id}.")
10
+ @original_error = original_error
11
+ @task_id = task_id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::TaskStatusUpdateFailed
6
+ class TaskStatusUpdateFailed < Base
7
+ attr_reader :task_id, :status
8
+ def initialize(task_id, status)
9
+ super("Task status cannot be updated task_id=#{task_id} status=#{status}.")
10
+ @task_id = task_id
11
+ @status = status
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::UnknownValidationOption
6
+ class UnknownValidationOption < Base
7
+ def initialize(option)
8
+ super("Validation option #{option} is unknown")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::InvalidQueueName
6
+ class UnsyncError < Base
7
+ def initialize
8
+ super('Not all tasks have been started yet')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::WorkflowDescriptorValidatorationError
6
+ class WorkflowDescriptorValidatorationError < Base
7
+ attr_reader :validation_errors
8
+ def initialize(validation_errors)
9
+ super('Invalid Job descriptor.')
10
+ @validation_errors = validation_errors
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Errors
5
+ # Qyu::Errors::WorkflowNotFound
6
+ class WorkflowNotFound < Base
7
+ attr_reader :key, :workflow_id
8
+ def initialize(key, workflow_id)
9
+ super("Workflow not found with #{key}=#{workflow_id}.")
10
+ @key = key
11
+ @workflow_id = workflow_id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ # Qyu::Factory
5
+ class Factory
6
+ class ServiceFactory
7
+ class << self
8
+ def register(adapter_class)
9
+ types[adapter_class::TYPE] = adapter_class
10
+ end
11
+
12
+ def types
13
+ @__types ||= {}
14
+ end
15
+
16
+ def get(config)
17
+ Qyu.logger.info "Got factory #{types[config[:type]]}"
18
+ types[config[:type]].new(config)
19
+ end
20
+ end
21
+ end
22
+
23
+ class QueueFactory < ServiceFactory; end
24
+ class StoreFactory < ServiceFactory; end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ (
4
+ Dir["#{File.dirname(__FILE__)}/models/concerns/*.rb"] +
5
+ Dir["#{File.dirname(__FILE__)}/models/*.rb"] +
6
+ Dir["#{File.dirname(__FILE__)}/models/enums/*.rb"]
7
+ ).each do |path|
8
+ require path
9
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Concerns
5
+ # Qyu::Concerns::WorkflowDescriptorValidator
6
+ class WorkflowDescriptorValidator
7
+ ALLOWED_KEYS = %w(queue waits_for starts starts_manually starts_with_params).freeze
8
+ attr_reader :errors
9
+
10
+ def initialize(descriptor)
11
+ @descriptor = descriptor
12
+ @errors = []
13
+ end
14
+
15
+ def valid?
16
+ validate
17
+ @errors.empty?
18
+ end
19
+
20
+ def validate
21
+ @errors << 'Descriptor type must be a Hash.' unless validate_descriptor_type
22
+ @errors << 'Entry points (starts) must be an Array.' unless validate_entry_points_type
23
+ @errors << 'Tasks must be a Hash.' unless validate_tasks_type
24
+ unless validate_entry_points_presence
25
+ @errors << 'There must be at least 1 entry point, and all entry points must exist in the tasks Hash.'
26
+ end
27
+ @errors << 'There must be at least 1 task in the tasks Hash.' unless validate_tasks_presence
28
+
29
+ tasks.keys.each do |task_name|
30
+ unless validate_queue_presence(task_name)
31
+ @errors << "#{task_name} must have a valid queue."
32
+ end
33
+ unless validate_task_keys(task_name)
34
+ @errors << "#{task_name} must only contain the following keys: #{ALLOWED_KEYS}."
35
+ end
36
+ unless validate_task_reference_formats(task_name)
37
+ @errors << "#{task_name} must follow the reference declaration format."
38
+ end
39
+ unless validate_task_references(task_name)
40
+ @errors << "#{task_name} must list existing tasks in its references."
41
+ end
42
+ unless validate_sync_condition_params(task_name)
43
+ @errors << "#{task_name} must pass the correct parameters to the sync task."
44
+ end
45
+ end
46
+ rescue => ex
47
+ Qyu.logger.error "Error while validation: #{ex.class}: #{ex.message}"
48
+ Qyu.logger.error "Backtrace: #{ex.backtrace.join("\n")}"
49
+ @errors << "#{ex.class}: #{ex.message}"
50
+ end
51
+
52
+ private
53
+
54
+ def validate_descriptor_type
55
+ @descriptor.is_a?(Hash)
56
+ end
57
+
58
+ def validate_entry_points_type
59
+ entry_points.is_a?(Array)
60
+ end
61
+
62
+ def validate_tasks_type
63
+ tasks.is_a?(Hash)
64
+ end
65
+
66
+ def validate_entry_points_presence
67
+ !entry_points.empty? && \
68
+ entry_points.all? { |task_name| tasks.keys.include?(task_name) }
69
+ end
70
+
71
+ def validate_tasks_presence
72
+ !tasks.empty?
73
+ end
74
+
75
+ def validate_queue_presence(task_name)
76
+ tasks[task_name]['queue'].is_a?(String)
77
+ end
78
+
79
+ def validate_task_keys(task_name)
80
+ tasks[task_name].keys.all? { |key| ALLOWED_KEYS.include?(key) }
81
+ end
82
+
83
+ def validate_task_reference_formats(task_name)
84
+ (tasks[task_name]['starts'].nil? || tasks[task_name]['starts'].is_a?(Array)) &&
85
+ (tasks[task_name]['starts_manually'].nil? || tasks[task_name]['starts_manually'].is_a?(Array)) &&
86
+ (tasks[task_name]['starts_with_params'].nil? || tasks[task_name]['starts_with_params'].is_a?(Hash))
87
+ (tasks[task_name]['waits_for'].nil? || tasks[task_name]['waits_for'].is_a?(Hash))
88
+ end
89
+
90
+ def validate_task_references(task_name)
91
+ (tasks[task_name]['starts'] || []).all? { |t_name| tasks[t_name].is_a?(Hash) } &&
92
+ (tasks[task_name]['starts_manually'] || []).all? { |t_name| tasks[t_name].is_a?(Hash) } &&
93
+ (tasks[task_name]['starts_with_params'] || {}).all? { |t_name, _| tasks[t_name].is_a?(Hash) }
94
+ (tasks[task_name]['waits_for'] || {}).all? { |t_name, _| tasks[t_name].is_a?(Hash) }
95
+ end
96
+
97
+ def validate_sync_condition_params(task_name)
98
+ return true unless tasks[task_name]['starts_with_params']
99
+ tasks[task_name]['starts_with_params'].all? do |started_task_name, params_config|
100
+ params_config.all? do |param_name, _param_config|
101
+ tasks[started_task_name]['waits_for'].detect do |_t_name, sync_config|
102
+ sync_config['condition']['param'] == param_name
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def entry_points
109
+ @descriptor['starts']
110
+ end
111
+
112
+ def tasks
113
+ @descriptor['tasks']
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ class Status
5
+ COMPLETED = 'completed'
6
+ QUEUED = 'queued'
7
+ WORKING = 'working'
8
+ FAILED = 'failed'
9
+ INVALID_PAYLOAD = 'invalid_payload'
10
+
11
+ def self.find(id)
12
+ Qyu.store.find_task(@id)
13
+ end
14
+
15
+ def initialize(id)
16
+ @id = id
17
+ end
18
+
19
+ def status
20
+ t = Qyu.store.find_task(@id)
21
+ t['status']
22
+ end
23
+
24
+ def completed?
25
+ status == COMPLETED
26
+ end
27
+
28
+ def queued?
29
+ status == QUEUED
30
+ end
31
+
32
+ def working?
33
+ status == WORKING
34
+ end
35
+
36
+ def failed?
37
+ status == FAILED
38
+ end
39
+
40
+ def invalid_payload?
41
+ status == INVALID_PAYLOAD
42
+ end
43
+ end
44
+ end