jobify 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: aa64c39d98c548e4f10f34c979871bf027a20f551b9b17d4c6961ea4288a7896
4
+ data.tar.gz: 784be3e225ff0a7405d315d799bb8b8d30e612fd5bc8a8b0fb765302c6099cd0
5
+ SHA512:
6
+ metadata.gz: f04966afa10e6d96dd9dbf6cb728d654c46bf3041bfbca4048dde85eb134894288b5e57a3642ff114f67a820fa8189853b97f692bc22a6ce4b7901a8d43ea84e
7
+ data.tar.gz: a6a86a55a01e7c465722ae779b9e6466c579a4731c074635d821b73ebbde7dc2ed453e0d52269149aa8d79561a08e8c3143905ff6e900986681913d7348fbaef
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ Exclude:
4
+ - 'test/*'
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: double_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ Enabled: true
12
+ EnforcedStyle: double_quotes
13
+
14
+ Metrics/BlockLength:
15
+ Max: 120
16
+
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+
20
+ Metrics/MethodLength:
21
+ Enabled: false
22
+
23
+ Layout/LineLength:
24
+ Max: 150
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2022-10-25
10
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in jobify.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 1.21"
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jobify (0.1.0)
5
+ activejob
6
+ activesupport
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activejob (7.0.4)
12
+ activesupport (= 7.0.4)
13
+ globalid (>= 0.3.6)
14
+ activesupport (7.0.4)
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ i18n (>= 1.6, < 2)
17
+ minitest (>= 5.1)
18
+ tzinfo (~> 2.0)
19
+ ast (2.4.2)
20
+ concurrent-ruby (1.1.10)
21
+ globalid (1.0.0)
22
+ activesupport (>= 5.0)
23
+ i18n (1.12.0)
24
+ concurrent-ruby (~> 1.0)
25
+ json (2.6.2)
26
+ minitest (5.16.3)
27
+ parallel (1.22.1)
28
+ parser (3.1.2.1)
29
+ ast (~> 2.4.1)
30
+ rainbow (3.1.1)
31
+ rake (13.0.6)
32
+ regexp_parser (2.6.0)
33
+ rexml (3.2.5)
34
+ rubocop (1.37.0)
35
+ json (~> 2.3)
36
+ parallel (~> 1.10)
37
+ parser (>= 3.1.2.1)
38
+ rainbow (>= 2.2.2, < 4.0)
39
+ regexp_parser (>= 1.8, < 3.0)
40
+ rexml (>= 3.2.5, < 4.0)
41
+ rubocop-ast (>= 1.22.0, < 2.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 1.4.0, < 3.0)
44
+ rubocop-ast (1.23.0)
45
+ parser (>= 3.1.1.0)
46
+ ruby-progressbar (1.11.0)
47
+ tzinfo (2.0.5)
48
+ concurrent-ruby (~> 1.0)
49
+ unicode-display_width (2.3.0)
50
+
51
+ PLATFORMS
52
+ x86_64-darwin-20
53
+ x86_64-linux
54
+
55
+ DEPENDENCIES
56
+ jobify!
57
+ minitest (~> 5.0)
58
+ rake (~> 13.0)
59
+ rubocop (~> 1.21)
60
+
61
+ BUNDLED WITH
62
+ 2.3.14
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Søren Houen
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Jobify
2
+
3
+ Run any method as a background job with `perform_my_method_later`. Works with both instance and class methods.
4
+
5
+ ## Why?
6
+
7
+ I think we as Rails developers are not using background processing as much as we could / should.
8
+
9
+ I believe this is largely because of 3 things:
10
+
11
+ - The extra work required to create a new MyTinyJob class
12
+ - The added complexity from moving code away from its natural and cohesive location in the project and into app/jobs
13
+ - The extra work required to _maintain_ a tiny MyTinyJob class
14
+
15
+ In short, the activation energy to use background jobs is too high.
16
+
17
+ Jobify lowers the activation energy to `jobify :my_method` to generate an activejob job class, and calling `perform_my_method_later`
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ class SomeClass
23
+ include Jobify
24
+
25
+ def self.my_method(arg1, kw_arg:)
26
+ puts "...#{arg1} #{kw_arg}'s which would be handy to do async..."
27
+ end
28
+ jobify :my_method
29
+ end
30
+
31
+ SomeClass.perform_my_method_later(42, kw_arg: 'flum')
32
+ ```
33
+
34
+ Output:
35
+
36
+ ```
37
+ [ActiveJob] Enqueued SomeClass::JobifyClassMethod_my_method_Job (Job ID: 8fc2ca69-cb31-4f73-a908-e98acb8a2832) to Async(default) with arguments: 42, {:kw_arg=>"flum"}
38
+ [ActiveJob] [SomeClass::JobifyClassMethod_my_method_Job] [8fc2ca69-cb31-4f73-a908-e98acb8a2832] Performing SomeClass::JobifyClassMethod_my_method_Job (Job ID: 8fc2ca69-cb31-4f73-a908-e98acb8a2832) from Async(default) enqueued at 2022-10-25T19:11:46Z with arguments: 42, {:kw_arg=>"flum"}
39
+ =>
40
+ #<SomeClass::JobifyClassMethod_my_method_Job:0x00007fee638d9cb8
41
+ @arguments=[42, {:kw_arg=>"flum"}],
42
+ @exception_executions={},
43
+ @executions=0,
44
+ @job_id="8fc2ca69-cb31-4f73-a908-e98acb8a2832",
45
+ @priority=nil,
46
+ @provider_job_id="8dc0614f-a229-4648-9308-4e6add9f666c",
47
+ @queue_name="default",
48
+ @successfully_enqueued=true,
49
+ @timezone=nil>
50
+ ...42 flum's which would be handy to do async...
51
+ [ActiveJob] [SomeClass::JobifyClassMethod_my_method_Job] [8fc2ca69-cb31-4f73-a908-e98acb8a2832] Performed SomeClass::JobifyClassMethod_my_method_Job (Job ID: 8fc2ca69-cb31-4f73-a908-e98acb8a2832) from Async(default) in 4.57ms
52
+ ```
53
+
54
+ ### Features
55
+
56
+ - Jobifies class methods
57
+ - Jobifies instance methods
58
+ - Verifies correct arguments are given when enqueing a job
59
+ - Small overhead added: 0.06 ms boot and 0.1ms on perform
60
+ - Override perform_xyz_later method with whatever name you prefer (eg. `jobify :my_method, name: :my_method_async`)
61
+
62
+ ### Instance methods
63
+
64
+ Instance methods work out of the box if your class inherits from `ApplicationRecord` or `ActiveRecord::Base`
65
+
66
+ If your class does not inherit from these it must supply `#id` and `.find(id)` methods to use `jobify` on
67
+ instance methods.
68
+
69
+ ### How it works
70
+ Calling `jobify :my_method` will declare two things:
71
+ - A new activejob job class as subclass of the class where it is called. This class `#perform` method will call the original method using `public_send`.
72
+ - A `perform_my_method_later` method, which will perform the job later.
73
+ - `perform_my_method_later` will raise unless given the same arguments as `my_method`.
74
+
75
+ #### Instance methods
76
+ Jobifying instance methods work by adding a special id keyword argument to `JobifyInstanceMethod_xyz_Job#perform`:
77
+
78
+ - When called, `#perform_xyz_later` gets the id of the instance via `instance#id`.
79
+ - The id is passed to the jobs `#perform` method as extra argument (`:__jobify__record_id`).
80
+ - `#perform` method uses this to find the record and then run the instance method on the record.
81
+
82
+ ## Installation
83
+
84
+ Install the gem and add to the application's Gemfile by executing:
85
+
86
+ $ bundle add jobify
87
+
88
+ If bundler is not being used to manage dependencies, install the gem by executing:
89
+
90
+ $ gem install jobify
91
+
92
+ ### Overhead
93
+
94
+ Benchmarks from running on a 2020 i7 Macbook Pro:
95
+
96
+ (Generate benchmarks by running `BENCHMARK=10_000 rake test` where env BENCHMARK is the number of iterations to run)
97
+ ```
98
+ Benchmark boot: 0.058 ms on avg of 100000 iterations
99
+ Benchmark perform: 0.228 ms on avg of 100000 iterations
100
+ Benchmark perform_control: 0.128 ms on avg of 100000 iterations
101
+ ```
102
+
103
+ **Boot overhead:**
104
+
105
+ Jobify adds ~ 0.06 ms avg overhead per call.
106
+ So if you used `jobify :something` 100 times in your code, you would add ~ 6 ms overhead to boot time.
107
+
108
+ **Run job overhead**
109
+
110
+ Jobify adds ~ 0.1 ms overhead to running a job. Performing via jobify takes ~ 0.23 ms versus ~ 0.13 ms for normal
111
+ ActiveJob execution.
112
+ For all but the most massively-scheduled-all-the-time jobs, this should be fine.
113
+
114
+ ## Development
115
+
116
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
117
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
118
+
119
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
120
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
121
+ push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
122
+
123
+ ### TODO
124
+ - Allow to add options to `jobify`, eg. `queue_as: :default`
125
+ - Allow to add global options, eg queue_as, prefix:, suffix:
126
+
127
+ ## Contributing
128
+
129
+ Bug reports and pull requests are welcome on GitHub at https://github.com/houen/jobify.
130
+
131
+ ## License
132
+
133
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jobify
4
+ VERSION = "0.1.0"
5
+ end
data/lib/jobify.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jobify/version"
4
+
5
+ require "active_support"
6
+ require "active_job"
7
+
8
+ class MissingArgument < ArgumentError; end
9
+
10
+ class MissingKeywordArgument < ArgumentError; end
11
+
12
+ # Include to allow running any method as a background job automatically
13
+ module Jobify
14
+ extend ActiveSupport::Concern
15
+
16
+ ID_ARG_NAME = :__jobify__record_id
17
+
18
+ included do
19
+ @jobified_methods = {
20
+ instance: {},
21
+ singleton: {}
22
+ }
23
+
24
+ def self.jobify(method_name, job_method_name: "perform_#{method_name}_later")
25
+ raise "method name cannot be blank" if method_name.blank?
26
+
27
+ method_name = method_name.to_s
28
+
29
+ if method_defined?(method_name) && !@jobified_methods[:instance][method_name]
30
+ # Instance method
31
+ params = instance_method(method_name).parameters
32
+ _define_job_class(method_name, job_method_name, params, false)
33
+ @jobified_methods[:instance][method_name] = true
34
+ elsif respond_to?(method_name) && !@jobified_methods[:singleton][method_name]
35
+ # Singleton method (Class method)
36
+ params = method(method_name).parameters
37
+ _define_job_class(method_name, job_method_name, params, true)
38
+ @jobified_methods[:singleton][method_name] = true
39
+ end
40
+ end
41
+
42
+ def self._define_job_class(method_name, job_method_name, params, singleton_method)
43
+ job_class_name = singleton_method ? "JobifyClassMethod_#{method_name}_Job" : "JobifyInstanceMethod_#{method_name}_Job"
44
+ parent_class = defined?(ApplicationJob) ? ApplicationJob : ActiveJob::Base
45
+ job_class = Class.new(parent_class)
46
+ caller_class = self
47
+ const_set(job_class_name, job_class)
48
+
49
+ # Define perform method on job class
50
+ if singleton_method
51
+ singleton__define_job_perform_method(job_class, caller_class, method_name)
52
+ singleton__define_job_enqueue_method(job_class, job_method_name, params)
53
+ else
54
+ instance__define_job_perform_method(job_class, caller_class, method_name)
55
+ instance__define_job_enqueue_method(job_class, job_method_name, params)
56
+ end
57
+ end
58
+
59
+ def self.singleton__define_job_perform_method(job_class, caller_class, method_name)
60
+ job_class.define_method(:perform) do |*args, **kw_args|
61
+ if kw_args.empty?
62
+ caller_class.public_send(method_name, *args)
63
+ else
64
+ caller_class.public_send(method_name, *args, **kw_args)
65
+ end
66
+ end
67
+ end
68
+
69
+ def self.singleton__define_job_enqueue_method(job_class, job_method_name, params)
70
+ define_singleton_method(job_method_name) do |*args, **kw_args|
71
+ req_args = params.filter_map { _1[0] == :req ? _1[1] : nil }
72
+ req_kw_args = params.filter_map { _1[0] == :keyreq ? _1[1] : nil }
73
+
74
+ ensure_required_kw_args_present!(req_kw_args, kw_args)
75
+ ensure_all_args_present!(req_args, args)
76
+
77
+ if kw_args.empty?
78
+ job_class.perform_later(*args)
79
+ else
80
+ job_class.perform_later(*args, **kw_args)
81
+ end
82
+ end
83
+ end
84
+
85
+ def self.instance__define_job_perform_method(job_class, caller_class, method_name)
86
+ job_class.define_method(:perform) do |*args, **kw_args|
87
+ id = kw_args.delete(ID_ARG_NAME)
88
+ raise "Something has gone wrong. Record id is required" unless id
89
+
90
+ record = caller_class.find(id)
91
+ record.public_send(method_name, *args, **kw_args)
92
+ end
93
+ end
94
+
95
+ def self.instance__define_job_enqueue_method(job_class, job_method_name, params)
96
+ define_method(job_method_name) do |*args, **kw_args|
97
+ req_args = params.filter_map { _1[0] == :req ? _1[1] : nil }
98
+ req_kw_args = params.filter_map { _1[0] == :keyreq ? _1[1] : nil }
99
+ self.class.ensure_required_kw_args_present!(req_kw_args, kw_args)
100
+ self.class.ensure_all_args_present!(req_args, args)
101
+
102
+ # Instance method adds
103
+ kw_args[ID_ARG_NAME] = id
104
+ job_class.perform_later(*args, **kw_args)
105
+ end
106
+ end
107
+
108
+ def self.ensure_required_kw_args_present!(req_kw_args, kw_args)
109
+ req_kw_args.each do |key|
110
+ next if kw_args.key?(key)
111
+
112
+ raise ::MissingKeywordArgument, "Missing require keyword argument `#{key}`"
113
+ end
114
+ end
115
+
116
+ def self.ensure_all_args_present!(req_args, args)
117
+ num_args_required = req_args.size
118
+ num_args_given = args.size
119
+
120
+ return if num_args_given >= num_args_required
121
+
122
+ raise ::MissingArgument, "Not enough arguments. Method expects #{num_args_required}. Got #{num_args_given}"
123
+ end
124
+
125
+ def ensure_required_kw_args_present!(req_kw_args, kw_args)
126
+ self.class.ensure_required_kw_args_present!(req_kw_args, kw_args)
127
+ end
128
+
129
+ def ensure_all_args_present!(req_args, args)
130
+ self.class.ensure_all_args_present!(req_args, args)
131
+ end
132
+ end
133
+ end
data/sig/jobify.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Jobify
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jobify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Søren Houen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-10-26 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: '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: activesupport
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
+ description:
42
+ email:
43
+ - s@houen.net
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rubocop.yml"
49
+ - CHANGELOG.md
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - lib/jobify.rb
56
+ - lib/jobify/version.rb
57
+ - sig/jobify.rbs
58
+ homepage: https://github.com/houen/jobify
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ allowed_push_host: https://rubygems.org
63
+ homepage_uri: https://github.com/houen/jobify
64
+ source_code_uri: https://github.com/houen/jobify
65
+ changelog_uri: https://github.com/houen/jobify/CHANGELOG.md
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.6.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.2.32
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Turn any method into a background job (`jobify :hello_world` generates `def
85
+ hello_world_job`
86
+ test_files: []