qyu 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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