tasque 0.0.3

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fe02e8efd7f9659a71065ff03456285a77c3738a
4
+ data.tar.gz: 0148d4b75800c0249624d34a50af1902857ad804
5
+ SHA512:
6
+ metadata.gz: 5446b19a1112207b663a3d91045da3c32f24d3fd4affd5d4adda441a22c412c27dc258a5f18a38f83e1409418c4cf9d8047b50c18fc7ba0f16c6cca96343b9a3
7
+ data.tar.gz: 6af7d2e232ef54594ee8a0f9971693bbcd22666cbcc251fa0b7b3cebc3c9bdd9b30ac15079f0b10c5b74ee713b11dab0c4ae993f29aae6fb5dd8a9948d37536d
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tasque.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Gropher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ # tasque
2
+
3
+ ActiveRecord based task queue. Task processing queue with states, history and priorities. Works with your favorite database.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's `Gemfile`:
9
+
10
+ gem 'tasque'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it manually:
17
+
18
+ $ gem install tasque
19
+
20
+ Then you need to generate initializer and migration:
21
+
22
+ $ rails g tasque:install
23
+
24
+ Don't forget to run migrations:
25
+
26
+ $ rake db:migrate
27
+
28
+
29
+ ## Usage
30
+
31
+ Create a simple task:
32
+
33
+ Tasque::Task.create! task: 'test', params: { text: 'This is the test!' }
34
+
35
+ Or a task with priority:
36
+
37
+ Tasque::Task.create! task: 'test', priority: 9000, params: { text: 'I will be processed first!!!' }
38
+
39
+ Or add a tag to find this task easily:
40
+
41
+ Tasque::Task.create! task: 'test', tag: 'user_123', params: { text: 'This task is property of User #123!' }
42
+
43
+ Now to process the task:
44
+
45
+ # create task processor
46
+ processor = Tasque::Processor.new
47
+
48
+ # register a handler for test tasks
49
+ processor.add_handler('test') do |task|
50
+ puts "Got task ##{task.id}. Task says: '#{task.params['text']}'"
51
+ { log: 'got task, printed text' } # returning task result
52
+ end
53
+
54
+ # start processor and wait
55
+ processor.start
56
+
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create new Pull Request
@@ -0,0 +1,25 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Tasque
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc 'Create a sample Tasque initializer and migration'
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ def self.next_migration_number(path)
12
+ unless @prev_migration_nr
13
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
14
+ else
15
+ @prev_migration_nr += 1
16
+ end
17
+ @prev_migration_nr.to_s
18
+ end
19
+
20
+ def create_initializer
21
+ template 'tasque.erb', 'config/initializers/tasque.rb'
22
+ migration_template "create_tasque_tasks.erb", "db/migrate/create_tasque_tasks.rb"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ class CreateTasqueTasks < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :tasque_tasks do |t|
4
+ t.string :tag
5
+ t.string :task
6
+ t.text :params
7
+ t.text :result
8
+ t.string :worker
9
+ t.integer :priority, default: 0
10
+ t.integer :attempts, default: 0
11
+ t.integer :progress, default: 0
12
+ t.string :status, default: 'new'
13
+
14
+ t.datetime :started_at
15
+ t.datetime :finished_at
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :tasque_tasks, [:status, :task]
21
+ add_index :tasque_tasks, :status
22
+ add_index :tasque_tasks, :tag
23
+ add_index :tasque_tasks, :task
24
+ add_index :tasque_tasks, :worker
25
+ add_index :tasque_tasks, :priority
26
+ add_index :tasque_tasks, :attempts
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ TASQUE_ROOT = Rails.root
2
+
3
+ Tasque.configure do |config|
4
+ config.logger = Rails.logger
5
+
6
+ # Which database from database.yml we use
7
+ config.environment = Rails.env
8
+
9
+ # This option need only if you want to receive tasks
10
+ # config.worker = 'example'
11
+
12
+ # How often should processor check for a new tasks
13
+ # config.check_interval = 10 # seconds
14
+
15
+ # How often should task progress be reported
16
+ # config.progress_interval = 5 # seconds
17
+
18
+ # This option tells processor to fetch only tasks with priority value greater than or equal to minimum_priority
19
+ # config.minimum_priority = 123
20
+
21
+ # Send task state updates and progress notifications via Insque
22
+ # config.notify = true
23
+
24
+ # Send worker heartbeat via Insque
25
+ # config.heartbeat = true
26
+ # config.heartbeat_interval = 30 # seconds
27
+ end
@@ -0,0 +1,10 @@
1
+ require "active_record"
2
+
3
+ require "tasque/configuration"
4
+ require "tasque/task"
5
+ require "tasque/task_error"
6
+ require "tasque/processor"
7
+ require "tasque/version"
8
+
9
+ module Tasque
10
+ end
@@ -0,0 +1,65 @@
1
+ require 'yaml'
2
+
3
+ module Tasque
4
+ class << self
5
+ attr_accessor :config
6
+ end
7
+
8
+ def self.configure
9
+ self.config ||= Configuration.new
10
+ yield(config) if block_given?
11
+ self.database_connection
12
+ return self.config
13
+ end
14
+
15
+ def self.database_connection()
16
+ raise 'No configuration. Use Tasque.configure' if self.config.nil?
17
+ @database ||= begin
18
+ if !defined?(Rails) && !ActiveRecord::Base.connected?
19
+ ActiveRecord::Base.logger = self.config.logger
20
+ db = self.config.database[self.config.environment.to_s]
21
+ db = self.config.database if db.nil?
22
+ ActiveRecord::Base.establish_connection(db)
23
+ ActiveRecord::Base.connection
24
+ end
25
+ ActiveRecord::Base
26
+ end
27
+ end
28
+
29
+ def self.root
30
+ defined?(TASQUE_ROOT) ? TASQUE_ROOT : Dir.pwd
31
+ end
32
+
33
+ class Configuration
34
+ attr_accessor :database
35
+ attr_accessor :database_file
36
+ attr_accessor :environment
37
+ attr_accessor :logger
38
+ attr_accessor :check_interval
39
+ attr_accessor :progress_interval
40
+ attr_accessor :minimum_priority
41
+ attr_accessor :worker
42
+ attr_accessor :heartbeat
43
+ attr_accessor :heartbeat_interval
44
+ attr_accessor :notify
45
+
46
+ def initialize
47
+ self.environment = :development
48
+ self.database_file = ::File.expand_path('config/database.yml', Tasque.root)
49
+ self.check_interval = 10 # seconds
50
+ self.worker = "default"
51
+ self.progress_interval = 5 # seconds
52
+ self.heartbeat = false
53
+ self.heartbeat_interval = 10 # seconds
54
+ self.notify = false
55
+ end
56
+
57
+ def database_file=(path)
58
+ if File.exists?(path)
59
+ @database = YAML.load(File.read(path)) || {}
60
+ @database_file = path
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,82 @@
1
+ require 'timers'
2
+
3
+ module Tasque
4
+ class Processor
5
+ attr_reader :timers
6
+
7
+ def initialize
8
+ @timers = Timers::Group.new
9
+ @last_task = false
10
+ @current_task = nil
11
+ @handlers = {}
12
+ end
13
+
14
+ def add_handler type, &block
15
+ @handlers[type.to_sym] = Proc.new do |task|
16
+ @current_task = task
17
+ task.process
18
+ begin
19
+ task.result = block.call task
20
+ rescue Tasque::TaskError => e
21
+ task.error = {
22
+ task_error: e.task_error
23
+ }
24
+ rescue Exception => e
25
+ task.error = {
26
+ exception: e.message,
27
+ backtrace: e.backtrace
28
+ }
29
+ end
30
+ task.error? ? task.failure : task.complete
31
+ @current_task = nil
32
+ end
33
+ @timers.every(check_interval) do
34
+ begin
35
+ has_task = Tasque::Task.fetch(type) do |task|
36
+ @handlers[type.to_sym].call(task)
37
+ end
38
+ end while has_task
39
+ end
40
+ end
41
+
42
+ def start
43
+ shutdown = ->(signo) {
44
+ if @last_task
45
+ unless @current_task.nil?
46
+ @current_task.failure
47
+ @current_task.reprocess
48
+ end
49
+ exit!
50
+ end
51
+ @last_task = true
52
+ }
53
+ trap("SIGINT", shutdown)
54
+ trap("SIGTERM", shutdown)
55
+ if Tasque.config.heartbeat && defined?(Insque)
56
+ heartbeat_thread = Thread.new do
57
+ heartbeat_timers = Timers::Group.new
58
+ heartbeat_timers.every(Tasque.config.heartbeat_interval) do
59
+ message = {
60
+ worker: Tasque.config.worker,
61
+ busy: !@current_job.nil?
62
+ }
63
+ Insque.broadcast :heartbeat, message
64
+ end
65
+ loop do
66
+ heartbeat_timers.wait
67
+ end
68
+ end
69
+ heartbeat_thread.abort_on_exception = true
70
+ end
71
+ loop do
72
+ break if @last_task
73
+ @timers.wait
74
+ end
75
+ end
76
+
77
+ private
78
+ def check_interval
79
+ Tasque.config.check_interval
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,139 @@
1
+ require 'state_machine/core'
2
+
3
+ module Tasque
4
+ class Task < ActiveRecord::Base
5
+ extend StateMachine::MacroMethods
6
+
7
+ MAX_ATTEMPTS=3
8
+
9
+ self.table_name = :tasque_tasks
10
+
11
+ serialize :params, JSON
12
+ serialize :result, JSON
13
+
14
+ scope :with_task, ->(task) { where(task: task).order priority: :desc }
15
+ scope :minimum_priority, ->(priority) { priority.nil? ? nil : where('priority >= ?', priority) }
16
+ scope :to_process, -> { where status: %w(new reprocessed) }
17
+ scope :with_error, -> { where status: 'error' }
18
+ scope :to_reprocess, -> { with_error.where 'attempts < ?', MAX_ATTEMPTS }
19
+ scope :finished_in, ->(interval) { where('finished_at > ?', interval.ago) }
20
+
21
+ validates :task, presence: true
22
+ validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
23
+ validates :progress, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
24
+ validates :priority, numericality: { only_integer: true }
25
+
26
+ class << self
27
+ def fetch(type, &block)
28
+ task = nil
29
+ transaction do
30
+ minimum_priority = Tasque.config.minimum_priority
31
+ task = self.with_task(type).to_process.minimum_priority(minimum_priority).lock(true).first
32
+ if task and task.can_pickup?
33
+ task.pickup
34
+ else
35
+ task = nil
36
+ end
37
+ end
38
+ yield(task) if task
39
+ !!task
40
+ end
41
+
42
+ def monitoring
43
+ {
44
+ queue: Tasque::Task.to_process.count,
45
+ errors: Tasque::Task.with_error.finished_in(1.hour).count
46
+ }
47
+ end
48
+
49
+ def autoreprocess(reprocess_limit = nil)
50
+ Tasque::Task.to_reprocess.limit(reprocess_limit.to_i).each do |task|
51
+ task.reprocess
52
+ end.count
53
+ end
54
+ end
55
+
56
+ state_machine :status, initial: :new do
57
+ after_transition on: :pickup do |task|
58
+ task.update_column :worker, Tasque.config.worker
59
+ end
60
+
61
+ after_transition on: :process do |task|
62
+ task.update_column :started_at, Time.now
63
+ end
64
+
65
+ after_transition on: :complete do |task|
66
+ task.update_columns progress: 100, finished_at: Time.now
67
+ end
68
+
69
+ after_transition on: :failure do |task|
70
+ task.update_columns attempts: (task.attempts + 1), result: { error: task.error }, progress: 0
71
+ end
72
+
73
+ after_transition on: :reprocess do |task|
74
+ task.update_columns started_at: nil, result: nil, progress: 0
75
+ end
76
+
77
+ after_transition do: :notify
78
+
79
+
80
+ event :pickup do
81
+ transition [:new, :reprocessed] => :starting
82
+ end
83
+
84
+ event :process do
85
+ transition :starting => :processing
86
+ end
87
+
88
+ event :complete do
89
+ transition :processing => :complete
90
+ end
91
+
92
+ event :failure do
93
+ transition :processing => :error
94
+ end
95
+
96
+ event :reprocess do
97
+ transition [:processing, :complete, :error] => :reprocessed
98
+ end
99
+
100
+ event :cancel do
101
+ transition any - [:processing] => :canceled
102
+ end
103
+
104
+
105
+ state :processing do
106
+ def progress!(val)
107
+ val = 0 if val < 0
108
+ val = 100 if val > 100
109
+ return if (Time.now.to_i - @last_progress_at.to_i) < Tasque.config.progress_interval || val == @last_progress_val
110
+ self.update_columns progress: val, updated_at: Time.now
111
+ @last_progress_at = Time.now
112
+ @last_progress_val = val
113
+ notify
114
+ end
115
+
116
+ def error!(task_error)
117
+ raise Tasque::TaskError.new(self, task_error)
118
+ end
119
+ end
120
+
121
+ state :processing, :error do
122
+ attr_accessor :error
123
+
124
+ def error?
125
+ !@error.nil?
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+ def notify
132
+ if Tasque.config.notify && defined?(Insque)
133
+ Insque.broadcast :task_update, self
134
+ end
135
+ rescue Exception => e
136
+ logger.error "Notify error: #{e.message}"
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,11 @@
1
+ module Tasque
2
+ class TaskError < Exception
3
+ attr_reader :task
4
+ attr_reader :task_error
5
+
6
+ def initialize(task, task_error)
7
+ @task = task
8
+ @task_error = task_error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Tasque
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tasque/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "tasque"
8
+ gem.version = Tasque::VERSION
9
+ gem.authors = ["Yuri Gomozov"]
10
+ gem.email = ["grophen@gmail.com"]
11
+ gem.description = %q{Task processing queue with states, history and priorities. Works with your favorite database. }
12
+ gem.summary = %q{ActiveRecord based task queue}
13
+ gem.homepage = "https://github.com/Gropher/tasque"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("activerecord")
21
+ gem.add_dependency("timers")
22
+ gem.add_dependency("state_machine")
23
+ gem.add_dependency("json")
24
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tasque
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Yuri Gomozov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: timers
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: state_machine
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: 'Task processing queue with states, history and priorities. Works with
70
+ your favorite database. '
71
+ email:
72
+ - grophen@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - lib/generators/tasque/install_generator.rb
82
+ - lib/generators/tasque/templates/create_tasque_tasks.erb
83
+ - lib/generators/tasque/templates/tasque.erb
84
+ - lib/tasque.rb
85
+ - lib/tasque/configuration.rb
86
+ - lib/tasque/processor.rb
87
+ - lib/tasque/task.rb
88
+ - lib/tasque/task_error.rb
89
+ - lib/tasque/version.rb
90
+ - tasque.gemspec
91
+ homepage: https://github.com/Gropher/tasque
92
+ licenses: []
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.5.1
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: ActiveRecord based task queue
114
+ test_files: []