jobify 0.1.0

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