hackler 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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +149 -0
- data/Rakefile +10 -0
- data/app/controllers/hackler/application_controller.rb +7 -0
- data/app/controllers/hackler/job_controller.rb +58 -0
- data/app/jobs/hackler/application_job.rb +6 -0
- data/app/jobs/hackler/hackler_job.rb +27 -0
- data/app/models/hackler/application_record.rb +7 -0
- data/app/models/hackler/job.rb +6 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20241002145356_create_hackler_jobs.rb +11 -0
- data/lib/active_job/queue_adapters/hackler_adapter.rb +29 -0
- data/lib/generators/hackler/install/USAGE +5 -0
- data/lib/generators/hackler/install/install_generator.rb +23 -0
- data/lib/generators/hackler/install/templates/initializer.rb +18 -0
- data/lib/generators/hackler/worker_install/USAGE +5 -0
- data/lib/generators/hackler/worker_install/templates/initializer.rb +22 -0
- data/lib/generators/hackler/worker_install/worker_install_generator.rb +13 -0
- data/lib/hackler/engine.rb +8 -0
- data/lib/hackler/version.rb +5 -0
- data/lib/hackler.rb +77 -0
- data/lib/tasks/hackler_tasks.rake +6 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4ee91a4d7a9915e7b074d7e6f882bfbc020de74875bba7827f018affb4fe4ed5
|
4
|
+
data.tar.gz: 18e8d50fa3213fd06ceeca5a0c2be32a4a062de969b62e61a39632131d0980b0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ecaabb2b8c2d08d2ed478f5f1ae1e6981d60f94a70e767abe79d56941df5579a958aabfadb12b45433a03900876450b6a8b849dd8a7ac7070f7d5f8f216ea6b3
|
7
|
+
data.tar.gz: 0c1b68a8578d5260c714dfd0772039fbdbbcb8fc28cb2ceae8191152cefe9fae393350db4e15ed51e18ef2b9d59f1d89dadae854e9ea392d1d2083eadb3dab02
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Jyrki Gadinger
|
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,149 @@
|
|
1
|
+
# Hackler
|
2
|
+
|
3
|
+
> **Hackler**, da (/ˈhaklɐ/)
|
4
|
+
>
|
5
|
+
> _(Austria, informal) hard worker_
|
6
|
+
|
7
|
+
A cursed approach to background jobs. **Here be dragons.**
|
8
|
+
|
9
|
+
Imagine you have a Rails app deployed to a web host that can **only** run Rack
|
10
|
+
applications backed by a MySQL database. There's no possibility to spin up
|
11
|
+
any form of containerised application (podman, docker), and no possibility to
|
12
|
+
drop into a shell (configure systemd units by hand, run stuff in a tmux/screen
|
13
|
+
session), nothing.
|
14
|
+
|
15
|
+
As long as you don't need any form of background processing, this is fine.
|
16
|
+
|
17
|
+
However, once you want to make use of background jobs you'll run into a few
|
18
|
+
challenges:
|
19
|
+
|
20
|
+
- Most (if not all) in-process adapters will lose their enqueued jobs when
|
21
|
+
the app is restarted, this is not suitable for production.
|
22
|
+
- Pretty much every other adapter requires either another process running
|
23
|
+
that's separate from the main app. Remember, our web host can only run
|
24
|
+
Rack-based apps!
|
25
|
+
- Most Active Job adapters usually require a specific database or message bus
|
26
|
+
too, be it PostgreSQL, Redis, RabbitMQ, beanstalkd, ... but all we have is
|
27
|
+
a basic MySQL.
|
28
|
+
- Running the extra worker process on another server requires access to the
|
29
|
+
main database, and the additional database the job library needs, this
|
30
|
+
complicates the configuration of those databases (firewalls, VPNs, ACLs)...
|
31
|
+
and on a shared web host you might not be able to expose the MySQL as you
|
32
|
+
wish.
|
33
|
+
- Running the extra worker process on another server requires an exact copy of
|
34
|
+
the app you're currently running, complicating deployment even further.
|
35
|
+
- Trying to start the extra worker process as part of the Rack app is even
|
36
|
+
more cursed than whatever this mess is. Not to mention that restarts are
|
37
|
+
going to get interesting...
|
38
|
+
- Let's face it: there are no other background job processors out there that
|
39
|
+
are cooler than Sidekiq.
|
40
|
+
- And migrating the app to another host is not a possibility either.
|
41
|
+
|
42
|
+
So what can we do?
|
43
|
+
|
44
|
+
Of course, we'll let our Rails web app talk to another Rails web app that can
|
45
|
+
make use of a real job processor, all over HTTP. *Obviously*.
|
46
|
+
|
47
|
+
## Sounds cursed, I'm in!
|
48
|
+
|
49
|
+
In the end you'll end up with two Rails apps.
|
50
|
+
|
51
|
+
One is your usual web app that will make use of a new kind of Active Job
|
52
|
+
adapter. Let's call it... `rails-web`.
|
53
|
+
|
54
|
+
The other one is a barebones Rails app that can use any other Active Job
|
55
|
+
adapter you like (yay, Sidekiq!). This one does not need access to the code
|
56
|
+
or database of `rails-web`, and therefore doesn't really care what jobs you
|
57
|
+
throw at it. I'll call that one `rails-worker`.
|
58
|
+
|
59
|
+
Whenever `rails-web` wants to enqueue an Active Job job, the `:hackler`
|
60
|
+
adapter will create a new `Hackler::Job` record and perform a HTTP POST
|
61
|
+
request to `rails-worker`, which will then enqueue a very basic job in the
|
62
|
+
adapter chosen by that app (probably `:sidekiq`). The basic job only has two
|
63
|
+
parameters: a base callback URL and the job ID.
|
64
|
+
|
65
|
+
Once the job processor framework used by `rails-worker` decides to enqueue the
|
66
|
+
job, the `rails-worker` makes a HTTP POST request to `rails-web`, which
|
67
|
+
fetches the stored job information from its database and runs the job. If
|
68
|
+
it's successful that job record is gone from the database. Should the job
|
69
|
+
fail however, the `rails-worker` process will automagically™ capture the
|
70
|
+
exception thrown by the job and re-raise it on the `rails-worker` side.
|
71
|
+
|
72
|
+
Of course, not everyone can access those HTTP endpoints exposed by Hackler,
|
73
|
+
so a shared secret must be defined on both ends. Some hashed value based on
|
74
|
+
that and the job info will be sent with each request.
|
75
|
+
|
76
|
+
Is this a good idea? Probably not, longer running jobs might block some
|
77
|
+
incoming requests. Should be fine for quick jobs though, like sending out
|
78
|
+
emails.
|
79
|
+
|
80
|
+
Is this extra cursed? **_Heck yea!_**
|
81
|
+
|
82
|
+
## Installation
|
83
|
+
|
84
|
+
Add the gem to your Gemfile:
|
85
|
+
|
86
|
+
```bash
|
87
|
+
$ bundle add hackler
|
88
|
+
```
|
89
|
+
|
90
|
+
In your main web app, install and configure Hackler:
|
91
|
+
|
92
|
+
```bash
|
93
|
+
$ bin/rails generate hackler:install
|
94
|
+
```
|
95
|
+
|
96
|
+
Make sure to edit `shared_secret`, `web_base_url`, and `worker_base_url` in
|
97
|
+
`config/initializers/hackler.rb`.
|
98
|
+
|
99
|
+
----
|
100
|
+
|
101
|
+
To install and configure Hackler in the worker app you first need to configure
|
102
|
+
an Active Job adapter. One that actually processes these jobs. I can't
|
103
|
+
remember which one I liked best, but I think some numbers will do fine as
|
104
|
+
well...
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# config/application.rb
|
108
|
+
|
109
|
+
class Application < Rails::Application
|
110
|
+
# ...
|
111
|
+
config.active_job.queue_adapter = 62060811362.to_s(36).to_sym
|
112
|
+
```
|
113
|
+
|
114
|
+
Then, install and configure Hackler:
|
115
|
+
|
116
|
+
```bash
|
117
|
+
$ bin/rails generate hackler:worker_install
|
118
|
+
```
|
119
|
+
|
120
|
+
Make sure to set `shared_secret` in `config/initializers/hackler.rb` to the
|
121
|
+
same value as on the web setup.
|
122
|
+
|
123
|
+
----
|
124
|
+
|
125
|
+
You are now ready to run the two Rails apps simultaneously! Use one terminal
|
126
|
+
per command:
|
127
|
+
|
128
|
+
```bash
|
129
|
+
# in your web app directory:
|
130
|
+
bin/rails server
|
131
|
+
|
132
|
+
# in your worker app directory:
|
133
|
+
bin/rails server -p 4000
|
134
|
+
bundle exec sidekiq
|
135
|
+
```
|
136
|
+
|
137
|
+
This repository contains an example Rails app with Sidekiq as its job backend
|
138
|
+
at `/hacklerer`.
|
139
|
+
|
140
|
+
## License
|
141
|
+
|
142
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
143
|
+
|
144
|
+
## That's cool and all, but should I use this?
|
145
|
+
|
146
|
+
Probably not. If you do, please let me know why.
|
147
|
+
|
148
|
+
As of now this library is a proof-of-concept. Or a prototype. Or an art
|
149
|
+
project. I mean, I didn't even bother with tests yet...
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackler
|
4
|
+
class JobController < ApplicationController
|
5
|
+
REQUIRED_PARAMS = %i[id base_url].freeze
|
6
|
+
|
7
|
+
before_action :set_secret
|
8
|
+
before_action :set_params
|
9
|
+
before_action :verify_secret!
|
10
|
+
|
11
|
+
def enqueue
|
12
|
+
Hackler::HacklerJob
|
13
|
+
.set(wait_until: Time.zone.at(@params[:timestamp]))
|
14
|
+
.perform_later(@params[:id], @params[:base_url])
|
15
|
+
render head: :no_content
|
16
|
+
end
|
17
|
+
|
18
|
+
def work
|
19
|
+
job = Hackler::Job.find(@params[:id])
|
20
|
+
ActiveJob::Base.execute(JSON.parse(job.data))
|
21
|
+
job.destroy!
|
22
|
+
render head: :no_content
|
23
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
24
|
+
# this needs to be a Exception, otherwise we won't catch e.g. `NoMethodError`s
|
25
|
+
render json: {
|
26
|
+
exception: {
|
27
|
+
class: e.class.name,
|
28
|
+
message: e.message,
|
29
|
+
backtrace: Hackler.backtrace_cleaner.call(e.backtrace),
|
30
|
+
},
|
31
|
+
}, status: :internal_server_error
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_secret
|
37
|
+
@header_secret = request.headers["x-hackler-secret"]
|
38
|
+
return if @header_secret
|
39
|
+
|
40
|
+
# pretend we're not there if the secret is missing
|
41
|
+
render head: :not_found
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_params
|
45
|
+
@params = params.permit(:id, :base_url, :timestamp).to_h.symbolize_keys
|
46
|
+
return if @params.keys.intersection(REQUIRED_PARAMS).size == 2
|
47
|
+
|
48
|
+
render json: { error: "required parameters are missing" }, status: :unprocessable_content
|
49
|
+
end
|
50
|
+
|
51
|
+
def verify_secret!
|
52
|
+
expected_secret = Hackler.build_secret(**@params)
|
53
|
+
return if @header_secret == expected_secret
|
54
|
+
|
55
|
+
render json: { error: "unauthorised" }, status: :unauthorized
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackler
|
4
|
+
class HacklerJob < ApplicationJob
|
5
|
+
queue_as Hackler.worker_queue
|
6
|
+
|
7
|
+
def perform(id, base_url)
|
8
|
+
conn = Hackler.connection_for(base_url)
|
9
|
+
parameters = {
|
10
|
+
id:,
|
11
|
+
base_url:,
|
12
|
+
}
|
13
|
+
begin
|
14
|
+
conn.post("work", parameters, { "x-hackler-secret" => Hackler.build_secret(**parameters) })
|
15
|
+
rescue Faraday::ServerError => e
|
16
|
+
json_response = begin
|
17
|
+
JSON.parse(e.response_body, symbolize_names: true)
|
18
|
+
rescue
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
raise Hackler::JobError.from_json(json_response[:exception]) if json_response.key?(:exception)
|
22
|
+
|
23
|
+
raise # reraise other unexpected errors
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module QueueAdapters
|
5
|
+
class HacklerAdapter < AbstractAdapter
|
6
|
+
def enqueue(job)
|
7
|
+
j = Hackler::Job.create(data: JSON.dump(job.serialize))
|
8
|
+
notify_worker(j)
|
9
|
+
end
|
10
|
+
|
11
|
+
def enqueue_at(job, timestamp)
|
12
|
+
j = Hackler::Job.create(data: JSON.dump(job.serialize))
|
13
|
+
notify_worker(j, timestamp)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def notify_worker(job, timestamp = nil)
|
19
|
+
conn = Hackler.connection_for(Hackler.worker_base_url)
|
20
|
+
parameters = {
|
21
|
+
id: job.id,
|
22
|
+
base_url: Hackler.web_base_url,
|
23
|
+
timestamp:,
|
24
|
+
}
|
25
|
+
conn.post("enqueue", parameters, { "x-hackler-secret" => Hackler.build_secret(**parameters) })
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Hackler::InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def copy_files
|
7
|
+
copy_file "initializer.rb", "config/initializers/hackler.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
def install_route
|
11
|
+
route %(mount Hackler::Engine => "/hackler")
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure_active_job_adapter
|
15
|
+
gsub_file Pathname(destination_root).join("config/environments/production.rb"),
|
16
|
+
/(# )?config\.active_job\.queue_adapter\s+=.*/,
|
17
|
+
"config.active_job.queue_adapter = :hackler"
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_migrations
|
21
|
+
rails_command "hackler:install:migrations"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Hackler.configure do |config|
|
4
|
+
# change this to an actual secret string
|
5
|
+
config.shared_secret = "hackme"
|
6
|
+
|
7
|
+
# `false` if this is the web app, `true` if this is the Hackler worker
|
8
|
+
config.worker = false
|
9
|
+
|
10
|
+
# set this to the base url where Hackler gets mounted in your app
|
11
|
+
config.web_base_url = "https://webapp.example.com/hackler"
|
12
|
+
|
13
|
+
# set this to where the Hackler worker runs
|
14
|
+
config.worker_base_url = "https://hackler.example.com/hackler"
|
15
|
+
|
16
|
+
# define a custom backtrace cleaner
|
17
|
+
# config.backtrace_cleaner = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Hackler.configure do |config|
|
4
|
+
# change this to an actual secret string
|
5
|
+
config.shared_secret = "hackme"
|
6
|
+
|
7
|
+
# `false` if this is the web app, `true` if this is the Hackler worker
|
8
|
+
config.worker = true
|
9
|
+
|
10
|
+
# queue name where Hackler jobs will be processed
|
11
|
+
config.worker_queue = :default
|
12
|
+
end
|
13
|
+
|
14
|
+
return # remove this line if you use Sidekiq and want to have some nice presets:
|
15
|
+
|
16
|
+
# record backtraces by default
|
17
|
+
Sidekiq.default_job_options = { backtrace: true } unless ENV.key?("SIDEKIQ_DISABLE_BACKTRACES")
|
18
|
+
|
19
|
+
Sidekiq.configure_server do |config|
|
20
|
+
# record full backtraces, they are cleaned by the hackler worker
|
21
|
+
config[:backtrace_cleaner] = ->(backtrace) { backtrace }
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Hackler::WorkerInstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def copy_files
|
7
|
+
copy_file "initializer.rb", "config/initializers/hackler.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
def install_route
|
11
|
+
route %(mount Hackler::Engine => "/hackler")
|
12
|
+
end
|
13
|
+
end
|
data/lib/hackler.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hackler/version"
|
4
|
+
require "hackler/engine"
|
5
|
+
|
6
|
+
require "active_job"
|
7
|
+
require "active_job/queue_adapters"
|
8
|
+
require "active_record"
|
9
|
+
|
10
|
+
require "digest/sha2"
|
11
|
+
require "faraday"
|
12
|
+
|
13
|
+
require "zeitwerk"
|
14
|
+
|
15
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
16
|
+
loader.ignore("#{__dir__}/generators")
|
17
|
+
loader.setup
|
18
|
+
|
19
|
+
module Hackler
|
20
|
+
mattr_accessor :shared_secret, default: "hackme"
|
21
|
+
|
22
|
+
# set to true if this is the instance that can actually run the jobs
|
23
|
+
mattr_accessor :worker, default: false
|
24
|
+
|
25
|
+
mattr_accessor :backtrace_cleaner, default: ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
|
26
|
+
|
27
|
+
mattr_accessor :web_base_url
|
28
|
+
|
29
|
+
mattr_accessor :worker_base_url
|
30
|
+
|
31
|
+
# set this to the queue hackler should use in jobs
|
32
|
+
mattr_accessor :worker_queue, default: :default
|
33
|
+
|
34
|
+
def self.configure = yield self
|
35
|
+
|
36
|
+
def self.build_secret(id:, base_url:, **)
|
37
|
+
Digest::SHA512.base64digest([shared_secret, id, base_url].join(&:to_s))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.connection_for(url) = Faraday.new(url:) do |builder|
|
41
|
+
builder.request :json
|
42
|
+
builder.response :json
|
43
|
+
builder.response :raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
# Base class for Hackler errors
|
47
|
+
class Error < StandardError; end
|
48
|
+
|
49
|
+
# Raised when a job failed
|
50
|
+
class JobError < Error
|
51
|
+
def self.from_json(json)
|
52
|
+
message = json.fetch(:message, "???")
|
53
|
+
klass = json.fetch(:class, "???")
|
54
|
+
backtrace = json.fetch(:backtrace) { ["???"] * 5 }
|
55
|
+
|
56
|
+
# sick ruby trick: create a new subclass and extend it with a module
|
57
|
+
# which overwrites the exception class' name, in order to make it look
|
58
|
+
# better in e.g. the sidekiq dashboards
|
59
|
+
subclass = Class.new(self).extend(NameExtender.new(klass))
|
60
|
+
subclass.new(message).tap do |exc|
|
61
|
+
exc.set_backtrace backtrace
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class NameExtender < Module
|
66
|
+
def initialize(name)
|
67
|
+
super
|
68
|
+
|
69
|
+
define_method :name do
|
70
|
+
name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private_constant :NameExtender
|
76
|
+
end
|
77
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hackler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jyrki Gadinger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.12'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 7.2.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 7.2.1
|
41
|
+
description: A cursed approach to background jobs. Here be dragons.
|
42
|
+
email:
|
43
|
+
- nilsding@nilsding.org
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- MIT-LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- app/controllers/hackler/application_controller.rb
|
52
|
+
- app/controllers/hackler/job_controller.rb
|
53
|
+
- app/jobs/hackler/application_job.rb
|
54
|
+
- app/jobs/hackler/hackler_job.rb
|
55
|
+
- app/models/hackler/application_record.rb
|
56
|
+
- app/models/hackler/job.rb
|
57
|
+
- config/routes.rb
|
58
|
+
- db/migrate/20241002145356_create_hackler_jobs.rb
|
59
|
+
- lib/active_job/queue_adapters/hackler_adapter.rb
|
60
|
+
- lib/generators/hackler/install/USAGE
|
61
|
+
- lib/generators/hackler/install/install_generator.rb
|
62
|
+
- lib/generators/hackler/install/templates/initializer.rb
|
63
|
+
- lib/generators/hackler/worker_install/USAGE
|
64
|
+
- lib/generators/hackler/worker_install/templates/initializer.rb
|
65
|
+
- lib/generators/hackler/worker_install/worker_install_generator.rb
|
66
|
+
- lib/hackler.rb
|
67
|
+
- lib/hackler/engine.rb
|
68
|
+
- lib/hackler/version.rb
|
69
|
+
- lib/tasks/hackler_tasks.rake
|
70
|
+
homepage: https://github.com/nilsding/hackler
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
metadata:
|
74
|
+
homepage_uri: https://github.com/nilsding/hackler
|
75
|
+
source_code_uri: https://github.com/nilsding/hackler
|
76
|
+
rubygems_mfa_required: 'true'
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.2.0
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubygems_version: 3.5.9
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: A cursed approach to background jobs
|
96
|
+
test_files: []
|