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