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,5 @@
1
+ require 'qyu/queue/base'
2
+ require 'qyu/queue/memory/adapter'
3
+
4
+ Qyu::Config::QueueConfig.register(Qyu::Queue::Memory::Adapter)
5
+ Qyu::Factory::QueueFactory.register(Qyu::Queue::Memory::Adapter)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Queue
5
+ # Qyu::Queue::Base
6
+ class Base
7
+ # This class acts as an interface for any queue adapter implemented for Qyu
8
+ # Implement the following methods in any queue and it should work seemlessly
9
+ def self.valid_config?(_config)
10
+ fail Qyu::Errors::NotImplementedError
11
+ end
12
+
13
+ # Instance methods
14
+ def enqueue_tasks(queue_name, task_ids)
15
+ task_ids.each do |task_id|
16
+ enqueue_task(queue_name, task_id)
17
+ end
18
+ end
19
+
20
+ # Instance methods to override
21
+ def enqueue_task(_queue_name, _task_id)
22
+ fail Qyu::Errors::NotImplementedError
23
+ end
24
+
25
+ def enqueue_task_to_failed_queue(_queue_name, _task_id)
26
+ fail Qyu::Errors::NotImplementedError
27
+ end
28
+
29
+ def fetch_next_message(_queue_name)
30
+ fail Qyu::Errors::NotImplementedError
31
+ end
32
+
33
+ def acknowledge_message(_queue_name, _message_id)
34
+ fail Qyu::Errors::NotImplementedError
35
+ end
36
+
37
+ def queues
38
+ fail Qyu::Errors::NotImplementedError
39
+ end
40
+
41
+ def size(_queue_name)
42
+ fail Qyu::Errors::NotImplementedError
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Queue
5
+ module Memory
6
+ class Adapter < Qyu::Queue::Base
7
+ TYPE = :memory
8
+
9
+ def initialize(_config)
10
+ @temp_store = Hash.new(false)
11
+ @queues = {}
12
+ @threads = []
13
+ end
14
+
15
+ def self.valid_config?(_config)
16
+ # TODO
17
+ true
18
+ end
19
+
20
+ def enqueue_task(queue_name, task_id)
21
+ queue(queue_name) << { 'task_id' => task_id }
22
+ end
23
+
24
+ def enqueue_task_to_failed_queue(queue_name, task_id)
25
+ failed_queue_name = queue_name + '-failed'
26
+ enqueue_task(failed_queue_name, task_id)
27
+ end
28
+
29
+ # fetch_next_message
30
+ #
31
+ # @param [String] queue_name
32
+ # @return [Hash] the acknowledge message
33
+ #
34
+ # TODO Note the uglyness in `while ... empty?`; it's because of reasons
35
+ # mainly for this (http://stackoverflow.com/q/11660253) reason.
36
+ def fetch_next_message(queue_name)
37
+ sleep(1) while queue(queue_name).empty?
38
+ message = queue(queue_name).pop(true)
39
+ message_id = Qyu::Utils.uuid
40
+ schedule_requeue(message, message_id, queue_name)
41
+ {
42
+ 'id' => message_id,
43
+ 'task_id' => message['task_id']
44
+ }
45
+ end
46
+
47
+ def acknowledge_message(_queue_name, message_id)
48
+ @temp_store[message_id] = true
49
+ end
50
+
51
+ def queues
52
+ @queues.map do |name, queue|
53
+ { name: name, messages: queue&.size }
54
+ end
55
+ end
56
+
57
+ def size(queue_name)
58
+ queue(queue_name).size
59
+ end
60
+
61
+ private
62
+
63
+ def schedule_requeue(message, message_id, queue_name)
64
+ @threads << Thread.new(message_id, message) do |t_message_id, t_message|
65
+ sleep(5)
66
+ queue(queue_name) << t_message unless message_acknowledged?(t_message_id)
67
+ end
68
+ end
69
+
70
+ def message_acknowledged?(message_id)
71
+ @temp_store[message_id] == true
72
+ end
73
+
74
+ # queue, or "get_or_create_queue"
75
+ #
76
+ # @param [String] name The name of the queue to create if it does
77
+ # does not exist and return;
78
+ def queue(name)
79
+ if @queues[name]
80
+ Qyu.logger.debug "Queue `#{name}`: #{@queues[name].length} elements"
81
+ return @queues[name]
82
+ end
83
+ Qyu.logger.info "Could not find queue `#{name}`, creating it"
84
+ @queues[name] ||= ::Queue.new
85
+ end
86
+ alias get_or_create_queue queue
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ require 'qyu/store/base'
2
+ require 'qyu/store/memory/adapter'
3
+
4
+ Qyu::Config::StoreConfig.register(Qyu::Store::Memory::Adapter)
5
+ Qyu::Factory::StoreFactory.register(Qyu::Store::Memory::Adapter)
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Store
5
+ # Qyu::Store::Base
6
+ class Base
7
+ # This class acts as an interface for any store implemented for Qyu
8
+ # Implement the following methods in any store and it should work seemlessly
9
+ def self.valid_config?(_config)
10
+ fail Qyu::Errors::NotImplementedError
11
+ end
12
+
13
+ def transaction
14
+ fail Qyu::Errors::NotImplementedError
15
+ end
16
+
17
+ ## Workflow
18
+ def persist_workflow(name, descriptor)
19
+ fail Qyu::Errors::NotImplementedError
20
+ end
21
+
22
+ def find_workflow(_id)
23
+ fail Qyu::Errors::NotImplementedError
24
+ end
25
+
26
+ def find_workflow_by_name(_name)
27
+ fail Qyu::Errors::NotImplementedError
28
+ end
29
+
30
+ def delete_workflow(_id)
31
+ fail Qyu::Errors::NotImplementedError
32
+ end
33
+
34
+ def delete_workflow_by_name(name)
35
+ fail Qyu::Errors::NotImplementedError
36
+ end
37
+
38
+ ## Job
39
+ def persist_job(_workflow, _payload)
40
+ fail Qyu::Errors::NotImplementedError
41
+ end
42
+
43
+ def find_job(_id)
44
+ fail Qyu::Errors::NotImplementedError
45
+ end
46
+
47
+ def select_jobs(_limit, _offset, _order = :asc)
48
+ fail Qyu::Errors::NotImplementedError
49
+ end
50
+
51
+ def count_jobs
52
+ fail Qyu::Errors::NotImplementedError
53
+ end
54
+
55
+ def delete_job(_id)
56
+ fail Qyu::Errors::NotImplementedError
57
+ end
58
+
59
+ def clear_completed_jobs
60
+ fail Qyu::Errors::NotImplementedError
61
+ end
62
+
63
+ ## Task
64
+
65
+ def find_or_persist_task(_name, _payload, _job_id, _parent_task_id)
66
+ fail Qyu::Errors::NotImplementedError
67
+ end
68
+
69
+ def find_task(_id)
70
+ fail Qyu::Errors::NotImplementedError
71
+ end
72
+
73
+ def find_task_ids_by_job_id_and_name(_job_id, _name)
74
+ fail Qyu::Errors::NotImplementedError
75
+ end
76
+
77
+ def find_task_ids_by_job_id_name_and_parent_task_ids(_job_id, _name, _parent_task_ids)
78
+ fail Qyu::Errors::NotImplementedError
79
+ end
80
+
81
+ def lock_task!(_id, _lease_time)
82
+ fail Qyu::Errors::NotImplementedError
83
+ end
84
+
85
+ def unlock_task!(_id, _lease_token)
86
+ fail Qyu::Errors::NotImplementedError
87
+ end
88
+
89
+ def renew_lock_lease(_id, _lease_time, _lease_token)
90
+ fail Qyu::Errors::NotImplementedError
91
+ end
92
+
93
+ def update_status(_id, _status)
94
+ fail Qyu::Errors::NotImplementedError
95
+ end
96
+
97
+ def task_status_counts(_job_id)
98
+ fail Qyu::Errors::NotImplementedError
99
+ end
100
+
101
+ def select_tasks_by_job_id
102
+ fail Qyu::Errors::NotImplementedError
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qyu
4
+ module Store
5
+ module Memory
6
+ class Adapter < Qyu::Store::Base
7
+ TYPE = :memory
8
+
9
+ def initialize(_config)
10
+ @workflows = {}
11
+ @jobs = {}
12
+ @tasks = {}
13
+ @locks = {}
14
+ @semaphore = Mutex.new
15
+ end
16
+
17
+ def self.valid_config?(_config)
18
+ # TODO
19
+ true
20
+ end
21
+
22
+ def find_workflow(id)
23
+ @workflows[id]
24
+ end
25
+
26
+ def find_workflow_by_name(name)
27
+ @workflows.detect do |_id, wflow|
28
+ wflow['name'] == name
29
+ end.last
30
+ end
31
+
32
+ def persist_workflow(name, descriptor)
33
+ id = Qyu::Utils.uuid
34
+ @workflows[id] = {
35
+ 'id' => id,
36
+ 'name' => name,
37
+ 'descriptor' => descriptor
38
+ }
39
+ id
40
+ end
41
+
42
+ def delete_workflow(id)
43
+ @workflows.delete(id)
44
+ end
45
+
46
+ def delete_workflow_by_name(name)
47
+ workflow = find_workflow_by_name(name)
48
+ return unless workflow
49
+ delete_workflow(workflow['id'])
50
+ end
51
+
52
+ def find_job(id)
53
+ @jobs[id]
54
+ end
55
+
56
+ def select_jobs(limit, offset, order = :asc)
57
+ ids = @jobs.keys[offset, limit]
58
+ selected = ids.map { |id| { id: id }.merge(@jobs[id]) }
59
+ return selected if order == :asc
60
+ selected.reverse
61
+ end
62
+
63
+ def persist_job(workflow, payload)
64
+ id = Qyu::Utils.uuid
65
+ @jobs[id] = {
66
+ 'payload' => payload,
67
+ 'workflow' => workflow
68
+ }
69
+ id
70
+ end
71
+
72
+ def delete_job(id)
73
+ @jobs.delete(id)
74
+ end
75
+
76
+ def clear_completed_jobs
77
+ # TODO
78
+ end
79
+
80
+ def count_jobs
81
+ @jobs.count
82
+ end
83
+
84
+ ## Task methods
85
+ def find_task(id)
86
+ @tasks[id]
87
+ end
88
+
89
+ def find_or_persist_task(name, queue_name, payload, job_id, parent_task_id)
90
+ matching_task = @tasks.detect do |_id, attrs|
91
+ attrs['job_id'] == job_id \
92
+ && attrs['name'] == name \
93
+ && attrs['payload'] == payload \
94
+ && attrs['queue_name'] == queue_name \
95
+ && attrs['parent_task_id'] == parent_task_id
96
+ end
97
+ return matching_task[0] if matching_task
98
+
99
+ id = Qyu::Utils.uuid
100
+ @tasks[id] = {
101
+ 'name' => name,
102
+ 'queue_name' => queue_name,
103
+ 'parent_task_id' => parent_task_id,
104
+ 'status' => Qyu::Status::QUEUED,
105
+ 'payload' => payload,
106
+ 'job_id' => job_id
107
+ }
108
+ yield(id)
109
+ id
110
+ end
111
+
112
+ def find_task_ids_by_job_id_and_name(job_id, name)
113
+ @tasks.select do |_id, attrs|
114
+ attrs['job_id'] == job_id && attrs['name'] == name
115
+ end.map { |(id, _attr)| id }
116
+ end
117
+
118
+ def find_task_ids_by_job_id_name_and_parent_task_ids(job_id, name, parent_task_ids)
119
+ @tasks.select do |_id, attrs|
120
+ attrs['job_id'] == job_id &&
121
+ attrs['name'] == name &&
122
+ parent_task_ids.include?(attrs['parent_task_id'])
123
+ end.map { |(id, _attr)| id }
124
+ end
125
+
126
+ def select_tasks_by_job_id(job_id)
127
+ @tasks.select { |_id, attrs| attrs['job_id'] == job_id }.map { |id, attrs| attrs.merge('id' => id) }
128
+ end
129
+
130
+ def task_status_counts(job_id)
131
+ # TODO
132
+ {}
133
+ end
134
+
135
+ def lock_task!(id, lease_time)
136
+ uuid = Qyu::Utils.uuid
137
+ locked = false
138
+ locked_until = nil
139
+ @semaphore.synchronize do
140
+ if @locks[id].nil? || @locks[id][:locked_until] < Time.now
141
+ locked_until = Qyu::Utils.seconds_after_time(lease_time)
142
+ @locks[id] = { locked_by: uuid, locked_until: locked_until }
143
+ locked = true
144
+ end
145
+ end
146
+
147
+ return [nil, nil] unless locked
148
+
149
+ [uuid, locked_until]
150
+ end
151
+
152
+ def unlock_task!(id, lease_token)
153
+ unlocked = false
154
+ @semaphore.synchronize do
155
+ if @locks[id][:locked_by] == lease_token
156
+ @locks.delete(id)
157
+ unlocked = true
158
+ end
159
+ end
160
+
161
+ unlocked
162
+ end
163
+
164
+ def update_status(id, status)
165
+ @tasks[id]['status'] = status
166
+ end
167
+
168
+ def renew_lock_lease(id, lease_time, lease_token)
169
+ locked_until = nil
170
+ @semaphore.synchronize do
171
+ if @locks[id][:locked_by] == lease_token && Time.now <= @locks[id][:locked_until]
172
+ locked_until = Qyu::Utils.seconds_after_time(lease_time)
173
+ @locks[id] = { locked_by: lease_token, locked_until: locked_until }
174
+ end
175
+ end
176
+
177
+ locked_until
178
+ end
179
+
180
+ def transaction
181
+ # TODO
182
+ yield
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra'
4
+ require 'qyu/ui/helpers/pagination'
5
+
6
+ module Qyu
7
+ class UI < Sinatra::Base
8
+ set :port, ENV['PORT'] || 3000
9
+ set :host, ENV['HOST'] || '0.0.0.0'
10
+
11
+ set :views, "#{__dir__}/ui/views"
12
+ set :public_folder, "#{__dir__}/ui/public"
13
+
14
+ include Qyu::Helpers::Pagination
15
+
16
+ get '/' do
17
+ redirect to('/jobs')
18
+ end
19
+
20
+ get '/jobs' do
21
+ page = params[:page].to_i > 0 ? params[:page].to_i : 1
22
+ limit = 10
23
+ offset = (page - 1) * limit
24
+
25
+ jobs = PaginatableArray.new(
26
+ Qyu::Job.select(limit: limit, offset: offset, order: :desc),
27
+ limit: limit, offset: offset, total_count: Qyu::Job.count,
28
+ page: page
29
+ )
30
+
31
+ erb :jobs, layout: true, locals: { jobs: jobs }
32
+ end
33
+
34
+ get '/jobs/:id' do
35
+ job = Qyu::Job.find(params[:id])
36
+ tasks_records = Qyu::Task.select(job_id: job.id)
37
+ total_count = tasks_records.count
38
+ task_statuses = job.task_status_counts
39
+ tasks = tasks_records.group_by(&:parent_task_id)
40
+ erb :show_job,
41
+ layout: true,
42
+ locals: {
43
+ job: job,
44
+ tasks: tasks,
45
+ total_count: total_count,
46
+ task_statuses: task_statuses
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def raw_html(value)
53
+ String.respond_to?(:html_safe) ? value.html_safe : value
54
+ end
55
+ end
56
+ end