active_job_store 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f576f48581d1561a8deb8c41fdbc1f89070bc7d48b5f3c6ece7e6de39c33747
4
+ data.tar.gz: a7807764c2199023daf083ee48ea3c5ded230f566bc07e25664e420ed048725c
5
+ SHA512:
6
+ metadata.gz: aa62be0ecc3e9ff53ccc8d3e1ff02202383323b0fba7075493bd5c5ca010e2393a627a7bd9e9b04edadc59c799d4fd7d630b81dbfcb3058d05601dce520777c8
7
+ data.tar.gz: 9436383c18415b52c7226816d62040cd8b8846c3385bac23681fa9730aadc98deebe086dfdfd2b8b33cedcbc5bdcffa951ae6f96725a3a49e74aa45770c0810e
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2022 Mattia Roccoberton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # ActiveJob Store
2
+
3
+ Persist job execution information on a support model `ActiveJobStore::Record`.
4
+
5
+ It can be useful to:
6
+ - improve jobs logging capabilities;
7
+ - query historical data about job executions;
8
+ - extract job's statistical data.
9
+
10
+ ## Installation
11
+
12
+ - Add to your Gemfile `gem 'active_job_store'` (and execute: `bundle`)
13
+ - Copy the gem migrations: `bundle exec rails active_job_store:install:migrations`
14
+ - Apply the new migrations: `bundle exec rails db:migrate`
15
+ - Add to your job `include ActiveJobStore` (or to your `ApplicationJob` class if you prefer)
16
+ - Access to the job executions data using the class method `job_executions` on your job (ex. `YourJob.job_executions`)
17
+
18
+ ## Usage examples
19
+
20
+ ```rb
21
+ SomeJob.perform_now(123)
22
+ SomeJob.perform_later(456)
23
+ SomeJob.set(wait: 1.minute).perform_later(789)
24
+ "SomeJob", "completed"]]
25
+ SomeJob.job_executions.first
26
+ # => #<ActiveJobStore::Record:0x00000001120f6320
27
+ # id: 1,
28
+ # job_id: "58daef7c-6b78-4d90-8043-39116eb9fe77",
29
+ # job_class: "SomeJob",
30
+ # state: "completed",
31
+ # arguments: [123],
32
+ # custom_data: nil,
33
+ # details: {"queue_name"=>"default", "priority"=>nil, "executions"=>1, "exception_executions"=>{}, "timezone"=>"UTC"},
34
+ # result: "some_result",
35
+ # exception: nil,
36
+ # enqueued_at: nil,
37
+ # started_at: Wed, 09 Nov 2022 21:09:50.611355000 UTC +00:00,
38
+ # completed_at: Wed, 09 Nov 2022 21:09:50.622797000 UTC +00:00,
39
+ # created_at: Wed, 09 Nov 2022 21:09:50.611900000 UTC +00:00>
40
+ ```
41
+
42
+ Extract some logs:
43
+
44
+ ```rb
45
+ puts ::ActiveJobStore::Record.order(id: :desc).pluck(:created_at, :job_class, :arguments, :state, :completed_at).map { _1.join(', ') }
46
+ # 2022-11-09 21:20:57 UTC, SomeJob, 123, completed, 2022-11-09 21:20:58 UTC
47
+ # 2022-11-09 21:18:26 UTC, AnotherJob, another test 2, completed, 2022-11-09 21:18:26 UTC
48
+ # 2022-11-09 21:13:18 UTC, SomeJob, Some test 3, completed, 2022-11-09 21:13:19 UTC
49
+ # 2022-11-09 21:12:18 UTC, SomeJob, Some test 2, error,
50
+ # 2022-11-09 21:10:13 UTC, AnotherJob, another test, completed, 2022-11-09 21:10:13 UTC
51
+ # 2022-11-09 21:09:50 UTC, SomeJob, Some test, completed, 2022-11-09 21:09:50 UTC
52
+ ```
53
+
54
+ Query jobs in a specific range of time:
55
+
56
+ ```rb
57
+ SomeJob.job_executions.where(started_at: 16.minutes.ago...).pluck(:job_id, :result, :started_at)
58
+ # => [["02beb3d6-a4eb-442c-8d78-29103ab894dc", "some_result", Wed, 09 Nov 2022 21:20:57.576018000 UTC +00:00],
59
+ # ["267e087e-cfa7-4c88-8d3b-9d40f912733f", "some_result", Wed, 09 Nov 2022 21:13:18.011484000 UTC +00:00]]
60
+ ```
61
+
62
+ Some statistics:
63
+
64
+ ```rb
65
+ SomeJob.job_executions.completed.map { |job| { id: job.id, execution_time: job.completed_at - job.started_at, started_at: job.started_at } }
66
+ # => [{:id=>6, :execution_time=>1.005239, :started_at=>Wed, 09 Nov 2022 21:20:57.576018000 UTC +00:00},
67
+ # {:id=>4, :execution_time=>1.004485, :started_at=>Wed, 09 Nov 2022 21:13:18.011484000 UTC +00:00},
68
+ # {:id=>1, :execution_time=>0.011442, :started_at=>Wed, 09 Nov 2022 21:09:50.611355000 UTC +00:00}]
69
+ ```
70
+
71
+ ## Customizations
72
+
73
+ If you need to store custom data, use `active_job_store_custom_data` accessor:
74
+
75
+ ```rb
76
+ class AnotherJob < ApplicationJob
77
+ include ActiveJobStore
78
+
79
+ def perform(some_id)
80
+ self.active_job_store_custom_data = []
81
+
82
+ active_job_store_custom_data << { time: Time.current, message: 'SomeJob step 1' }
83
+ sleep 1
84
+ active_job_store_custom_data << { time: Time.current, message: 'SomeJob step 2' }
85
+
86
+ 'some_result'
87
+ end
88
+ end
89
+
90
+ AnotherJob.perform_now(123)
91
+ AnotherJob.job_executions.last.custom_data
92
+ # => [{"time"=>"2022-11-09T21:20:57.580Z", "message"=>"SomeJob step 1"}, {"time"=>"2022-11-09T21:20:58.581Z", "message"=>"SomeJob step 2"}]
93
+ ```
94
+
95
+ If for any reason it's needed to process the result before storing it, just override `active_job_store_format_result`:
96
+
97
+ ```rb
98
+ class AnotherJob < ApplicationJob
99
+ include ActiveJobStore
100
+
101
+ def perform(some_id)
102
+ 42
103
+ end
104
+
105
+ def active_job_store_format_result(result)
106
+ result * 2
107
+ end
108
+ end
109
+
110
+ AnotherJob.perform_now(123)
111
+ AnotherJob.job_executions.last.result
112
+ # => 84
113
+ ```
114
+
115
+ ## Do you like it? Star it!
116
+
117
+ If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
118
+
119
+ Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
120
+
121
+ ## Contributors
122
+
123
+ - [Mattia Roccoberton](https://blocknot.es/): author
124
+
125
+ ## License
126
+
127
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJobStore
4
+ class Record < ApplicationRecord
5
+ self.table_name = 'active_job_store'
6
+
7
+ serialize :arguments, JSON
8
+ serialize :custom_data, JSON
9
+ serialize :details, JSON
10
+ serialize :result, JSON
11
+
12
+ enum state: { initialized: 0, enqueued: 1, started: 2, completed: 3, error: 4 }
13
+
14
+ scope :by_arguments, ->(*args) { where('arguments = ?', args.to_json) }
15
+ scope :performing, -> { where(state: %i[enqueued started]) }
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveJobStore < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :active_job_store do |t|
6
+ t.string :job_id, null: false
7
+ t.string :job_class, null: false
8
+ t.integer :state, null: false
9
+ t.text :arguments
10
+ t.text :custom_data
11
+ t.text :details
12
+ t.text :result
13
+ t.string :exception
14
+ t.datetime :enqueued_at
15
+ t.datetime :started_at
16
+ t.datetime :completed_at
17
+ t.datetime :created_at
18
+ end
19
+
20
+ add_index :active_job_store, [:job_class, :job_id], unique: true
21
+ add_index :active_job_store, [:job_class, :state]
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJobStore
4
+ class Engine < ::Rails::Engine
5
+ engine_name 'active_job_store'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJobStore
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_job_store/engine'
4
+
5
+ module ActiveJobStore
6
+ IGNORE_ATTRS = %w[arguments job_id successfully_enqueued].freeze
7
+
8
+ attr_accessor :active_job_store_custom_data
9
+
10
+ class << self
11
+ def included(base)
12
+ base.extend(ClassMethods)
13
+
14
+ base.around_enqueue do |job, block|
15
+ store_record = ::ActiveJobStore::Record.find_or_create_by!(job.active_job_store_reference) do |record|
16
+ record.arguments = job.arguments
17
+ record.details = job.as_json.except(*IGNORE_ATTRS)
18
+ record.state = :initialized
19
+ end
20
+ store_record.lock! # NOTE: needed to avoid update conflicts with perform when setting the state to enqueued
21
+ block.call
22
+ store_record.update!(state: :enqueued, enqueued_at: Time.current)
23
+ end
24
+
25
+ base.around_perform do |job, block|
26
+ store_record = ::ActiveJobStore::Record.find_or_initialize_by(job.active_job_store_reference) do |record|
27
+ record.arguments = job.arguments
28
+ end
29
+ store_record.update!(details: job.as_json.except(*IGNORE_ATTRS), state: :started, started_at: Time.current)
30
+ result = block.call
31
+ result_data = job.active_job_store_format_result(result)
32
+ store_record.update!(state: :completed, completed_at: Time.current, result: result_data, custom_data: active_job_store_custom_data)
33
+ rescue StandardError => e
34
+ store_record.update!(state: :error, exception: e.inspect)
35
+ raise
36
+ end
37
+ end
38
+ end
39
+
40
+ def active_job_store_format_result(result)
41
+ result
42
+ end
43
+
44
+ def active_job_store_reference
45
+ { job_id: job_id, job_class: self.class.to_s }
46
+ end
47
+
48
+ module ClassMethods
49
+ def job_executions
50
+ ::ActiveJobStore::Record.where(job_class: to_s)
51
+ end
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_job_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattia Roccoberton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-11-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ description: ActiveJob Store permits to store jobs state and custom data on a database
28
+ email: mat@blocknot.es
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - LICENSE.txt
34
+ - README.md
35
+ - app/models/active_job_store/record.rb
36
+ - db/migrate/20221101010101_create_active_job_store.rb
37
+ - lib/active_job_store.rb
38
+ - lib/active_job_store/engine.rb
39
+ - lib/active_job_store/version.rb
40
+ homepage: https://github.com/blocknotes/active_job_store
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/blocknotes/active_job_store
45
+ source_code_uri: https://github.com/blocknotes/active_job_store
46
+ changelog_uri: https://github.com/blocknotes/active_job_store/blob/master/CHANGELOG.md
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.6.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.1.6
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Persist jobs information on DB
67
+ test_files: []