rust_on_background 0.1.1

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: d02d4e6d02e8cd7bc23d630c36fe81cca4ad224d34f5e03a5fb418f06bac6edf
4
+ data.tar.gz: 010bd6fd680da034fcead075f2c85cb7e243a823b00f0d1877926eea20af973a
5
+ SHA512:
6
+ metadata.gz: ad6725af3e3e3cda056251d51e09680c38587ffa4d4e88c2aea442669c55b2a51b90fcd3df4d3ee5867fd5e1862f2a60f95c0d48a1e4a30ebdc1fbccfc7192cf
7
+ data.tar.gz: d66d46c7032f3d0955e0a0ceafc499422d87c8d55599dafd61e50de346da0e1813cfb2d04df31b56a1f1a547b3ecad6fbb0ec75055a0fab495b2498cc6511a6e
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-03-13
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 QuarkXZ
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,338 @@
1
+ # RustOnBackground
2
+
3
+ Run background jobs in Rust. Run a few commands and you're all set up - just scaffold and write Rust. [Jump to quick start](#quick-start)
4
+
5
+ ```ruby
6
+ # Rails: enqueue a job
7
+ RustOnBackground.perform("send_email", user_id: 123, template: "welcome")
8
+
9
+ # Schedule for later
10
+ RustOnBackground.perform_at(1.hour.from_now, "cleanup", batch_size: 100)
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - Rails generators for instant setup, very easy
16
+ - Auto-detects database adapter (MySQL, PostgreSQL, SQLite) from your database.yml config
17
+ - Scaffolding: Auto-generates Rust structs from ActiveRecord models (You can do just partial scaffold only to create the structs from schema)
18
+ - Multiple queues with configurable worker counts
19
+ - Scheduled jobs with `perform_at`
20
+ - Environment-specific configs (`config.development.toml`, `config.production.toml`)
21
+ - Automatic retries with configurable retry count
22
+ - Rake tasks for everything: `build`, `start`, `stop`, `restart`, `status`, `rebuild`
23
+
24
+ ## Requirements
25
+
26
+ - Ruby 2.5+
27
+ - Rails 5.0+
28
+ - Redis 4.0+
29
+ - Rust 1.70+ ([install](https://rustup.rs))
30
+
31
+ ## Quick Start
32
+
33
+ Add to your Gemfile:
34
+
35
+ ```ruby
36
+ gem "rust_on_background"
37
+ ```
38
+
39
+ Run bundle install:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ This creates the Rust project structure at `app/jobs/rust/` and detects your database configuration automatically:
46
+
47
+ ```bash
48
+ rails generate rust_on_background:install
49
+ ```
50
+
51
+
52
+ ```bash
53
+ # Create a job (generates app/jobs/rust/src/jobs/send_email.rs)
54
+ rails generate rust_on_background:job send_email user_id:integer message:string
55
+
56
+ # Build
57
+ rake rust_on_background:build
58
+
59
+ # Start workers
60
+ rake rust_on_background:start
61
+
62
+ # Enqueue from Rails
63
+ RustOnBackground.perform("send_email", user_id: 1, message: "welcome")
64
+ ```
65
+
66
+ ## Project Structure
67
+
68
+ After installation, your Rails app will have:
69
+
70
+ ```
71
+ app/jobs/rust/
72
+ ├── Cargo.toml # Rust dependencies (add crates here)
73
+ ├── config.development.toml # Dev config (auto-generated)
74
+ ├── config.production.toml # Create this for production
75
+ └── src/
76
+ ├── main.rs # Don't need to touch this
77
+ ├── worker.rs # Don't need to touch this
78
+ ├── scheduler.rs # Don't need to touch this
79
+ └── jobs/ # YOUR CODE GOES HERE
80
+ ├── mod.rs # Auto-updated by generator, this is basically a controller for the jobs(scaffolding creates the action for you in here)
81
+ └── send_email.rs # Example job file
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ Environment-specific config files in `app/jobs/rust/`:
87
+
88
+ ```toml
89
+ # config.development.toml
90
+
91
+ [database]
92
+ url = "mysql://root@localhost/myapp_development"
93
+
94
+ [redis]
95
+ url = "redis://127.0.0.1:6379"
96
+
97
+ [logging]
98
+ path = "log/development.log" # can leave empty for no logs
99
+
100
+ # Define your queues
101
+ [[queues]]
102
+ name = "default"
103
+ workers = 4
104
+
105
+ [[queues]]
106
+ name = "emails"
107
+ workers = 2
108
+
109
+ # Scheduler settings (optional)
110
+ # [scheduler]
111
+ # poll_interval_ms = 1000
112
+ # batch_size = 100
113
+ ```
114
+
115
+ ## Generating Jobs
116
+
117
+ ### Basic job
118
+
119
+ ```bash
120
+ rails generate rust_on_background:job cleanup
121
+ ```
122
+
123
+ ### Job with typed arguments
124
+
125
+ ```bash
126
+ rails generate rust_on_background:job send_email user_id:integer template:string urgent:boolean
127
+ ```
128
+
129
+ Generates:
130
+
131
+ ```rust
132
+ #[derive(Debug, Deserialize)]
133
+ struct Args {
134
+ user_id: i64,
135
+ template: String,
136
+ urgent: bool,
137
+ }
138
+
139
+ pub async fn run(args: &serde_json::Value, _database_url: &str) -> JobResult {
140
+ let args: Args = serde_json::from_value(args.clone())?;
141
+ // Your logic here
142
+ JobResult::Success
143
+ }
144
+ ```
145
+
146
+ ### Job with ActiveRecord model
147
+
148
+ ```bash
149
+ rails generate rust_on_background:job process_order order:Order
150
+ ```
151
+
152
+ The generator reads your model's columns from the database and creates a matching Rust struct:
153
+
154
+ ```rust
155
+ #[derive(Debug, Deserialize)]
156
+ struct Order {
157
+ id: i64,
158
+ user_id: i64,
159
+ total: f64,
160
+ status: String,
161
+ created_at: String,
162
+ updated_at: String,
163
+ }
164
+
165
+ #[derive(Debug, Deserialize)]
166
+ struct Args {
167
+ order: Order,
168
+ }
169
+ ```
170
+
171
+ ## Enqueuing Jobs
172
+
173
+ ### Immediate execution
174
+
175
+ ```ruby
176
+ RustOnBackground.perform("job_name", arg1: "value", arg2: 123) # (default queue if not specified)
177
+ ```
178
+
179
+ ### With specific queue
180
+
181
+ ```ruby
182
+ RustOnBackground.perform("send_email", queue: "emails", user_id: 1)
183
+ ```
184
+
185
+ ### With retry count
186
+
187
+ ```ruby
188
+ RustOnBackground.perform("flaky_job", retry_count: 5, data: "...")
189
+ ```
190
+
191
+ ### Scheduled execution
192
+
193
+ ```ruby
194
+ # Run in 1 hour
195
+ RustOnBackground.perform_at(1.hour.from_now, "cleanup")
196
+
197
+ # Run at specific time
198
+ RustOnBackground.perform_at(Date.tomorrow.noon, "daily_report")
199
+
200
+ # With all options
201
+ RustOnBackground.perform_at(
202
+ 30.minutes.from_now,
203
+ "send_reminder",
204
+ queue: "emails",
205
+ retry_count: 3,
206
+ user_id: 123
207
+ )
208
+ ```
209
+
210
+ ## Supported Types
211
+
212
+ | Ruby/Rails Type | Rust Type |
213
+ |-----------------|-----------|
214
+ | `string`, `text` | `String` |
215
+ | `integer`, `bigint` | `i64` |
216
+ | `float`, `decimal` | `f64` |
217
+ | `boolean` | `bool` |
218
+ | `array`, `set` | `Vec<serde_json::Value>` |
219
+ | `hash`, `json`, `jsonb` | `serde_json::Value` |
220
+ | `date`, `datetime`, `time` | `String` |
221
+ | `uuid` | `String` |
222
+ | `binary`, `blob` | `Vec<u8>` |
223
+ | ActiveRecord model | Auto-generated struct |
224
+
225
+ ## Rake Tasks
226
+
227
+ ```bash
228
+ # Build the Rust binary
229
+ rake rust_on_background:build
230
+
231
+ # Start all workers
232
+ rake rust_on_background:start
233
+
234
+ # Start specific queue
235
+ rake rust_on_background:start[emails]
236
+
237
+ # Stop all workers
238
+ rake rust_on_background:stop
239
+
240
+ # Stop specific queue
241
+ rake rust_on_background:stop[emails]
242
+
243
+ # Restart workers
244
+ rake rust_on_background:restart
245
+
246
+ # Stop, rebuild, and start - for example when doing a deploy
247
+ rake rust_on_background:rebuild
248
+
249
+ # Check status
250
+ rake rust_on_background:status
251
+
252
+ # Start scheduler (run ONE instance per Redis db)
253
+ rake rust_on_background:scheduler:start
254
+
255
+ # Stop scheduler
256
+ rake rust_on_background:scheduler:stop
257
+ ```
258
+
259
+ ## Writing Job Logic
260
+
261
+ Jobs are in `app/jobs/rust/src/jobs/`. Each job is a Rust module with a `run` function:
262
+
263
+ ```rust
264
+ use serde::Deserialize;
265
+ use crate::worker::JobResult;
266
+
267
+ #[derive(Debug, Deserialize)]
268
+ struct Args {
269
+ user_id: i64,
270
+ message: String,
271
+ }
272
+
273
+ pub async fn run(args: &serde_json::Value, database_url: &str) -> JobResult {
274
+ // Parse args
275
+ let args: Args = match serde_json::from_value(args.clone()) {
276
+ Ok(a) => a,
277
+ Err(e) => return JobResult::Failed(format!("Invalid args: {}", e)),
278
+ };
279
+
280
+ // Your logic here
281
+ tracing::info!("Processing user {}", args.user_id);
282
+
283
+ // Return result
284
+ JobResult::Success
285
+ // or: JobResult::Retry("temporary failure".to_string())
286
+ // or: JobResult::Failed("permanent failure".to_string())
287
+ }
288
+ ```
289
+
290
+ ### Database access
291
+
292
+ The `database_url` parameter gives you direct access to your Rails database:
293
+
294
+ ```rust
295
+ use sqlx::mysql::MySqlPool;
296
+
297
+ pub async fn run(args: &serde_json::Value, database_url: &str) -> JobResult {
298
+ let pool = MySqlPool::connect(database_url).await?;
299
+
300
+ let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE active = ?")
301
+ .bind(true)
302
+ .fetch_all(&pool)
303
+ .await?;
304
+
305
+ JobResult::Success
306
+ }
307
+ ```
308
+
309
+ ### HTTP callbacks to Rails (example)
310
+
311
+ If you need to notify Rails when a job completes, you can use any HTTP client. Example with `reqwest`:
312
+
313
+ ```rust
314
+ pub async fn run(args: &serde_json::Value, _database_url: &str) -> JobResult {
315
+ // Do work...
316
+
317
+ // Notify Rails
318
+ reqwest::Client::new()
319
+ .post("http://localhost:3000/webhooks/job_complete")
320
+ .json(&serde_json::json!({ "job": "my_job", "status": "done" }))
321
+ .send()
322
+ .await?;
323
+
324
+ JobResult::Success
325
+ }
326
+ ```
327
+
328
+ ## Production Deployment
329
+
330
+ 1. Create `app/jobs/rust/config.production.toml` with production database/Redis URLs
331
+ 2. `rake rust_on_background:rebuild` (stops workers, builds, starts workers)
332
+ 3. `rake rust_on_background:scheduler:start`
333
+
334
+ Workers run as background processes with PID files in `tmp/pids/`.
335
+
336
+ ## License
337
+
338
+ MIT License. See [LICENSE](LICENSE) for details.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,174 @@
1
+ require "rails/generators"
2
+ require "yaml"
3
+ require "erb"
4
+
5
+ module RustOnBackground
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a basic structure inside of your project"
11
+
12
+ def create_rust_jobs_dir
13
+ empty_directory "app/jobs/rust"
14
+ empty_directory "app/jobs/rust/src"
15
+ empty_directory "app/jobs/rust/src/jobs"
16
+ end
17
+
18
+ def copy_cargo_toml
19
+ @db_adapter = detect_database_adapter
20
+ @db_feature = database_feature_for(@db_adapter)
21
+ template "Cargo.toml.erb", "app/jobs/rust/Cargo.toml"
22
+ end
23
+
24
+ def copy_rust_source_files
25
+ copy_file "main.rs", "app/jobs/rust/src/main.rs"
26
+ copy_file "worker.rs", "app/jobs/rust/src/worker.rs"
27
+ copy_file "scheduler.rs", "app/jobs/rust/src/scheduler.rs"
28
+ copy_file "jobs/mod.rs", "app/jobs/rust/src/jobs/mod.rs"
29
+ end
30
+
31
+ def create_config_file
32
+ @database_url = build_database_url
33
+ template "config.toml.erb", "app/jobs/rust/config.#{Rails.env}.toml"
34
+ end
35
+
36
+ def create_rake_tasks
37
+ copy_file "rust_on_background.rake", "lib/tasks/rust_on_background.rake"
38
+ end
39
+
40
+ def update_gitignore
41
+ gitignore_path = Rails.root.join(".gitignore")
42
+ return unless File.exist?(gitignore_path)
43
+
44
+ content = File.read(gitignore_path)
45
+ return if content.include?("rust_on_background")
46
+
47
+ append_to_file ".gitignore", <<~GITIGNORE
48
+
49
+ # rust_on_background
50
+ /app/jobs/rust/target
51
+ /app/jobs/rust/Cargo.lock
52
+ GITIGNORE
53
+
54
+ say "Updated .gitignore with rust_on_background /app/jobs/rust/target and /app/jobs/rust/Cargo.lock", :green
55
+ end
56
+
57
+ def show_post_install_message
58
+ say ""
59
+ say "Rust jobs created successfully!", :green
60
+ say ""
61
+ say "Configuration:"
62
+ say " Database: Using config/database.yml (#{detect_database_adapter})"
63
+ say " Config: app/jobs/rust/config.#{Rails.env}.toml"
64
+ say ""
65
+ say "Next steps:"
66
+ say " 1. Review app/jobs/rust/config.#{Rails.env}.toml"
67
+ say " 2. Generate a file for some job, example: rails generate rust_on_background:job import_big_xlsx filepath:string"
68
+ say " 3. Write the logic in the job file you created somewhere at: app/jobs/rust/src/jobs/import_big_xlsx.rs"
69
+ say " 4. Call it from somewhere in your app RustOnBackground.perform('import_big_xlsx', filepath: '/path/to/file.xlsx')"
70
+ say " 5. Compile: rake rust_on_background:build"
71
+ say " 6. Start the background jobs: rake rust_on_background:start"
72
+ say ""
73
+ say "All rake tasks available:"
74
+ say " rake rust_on_background:build"
75
+ say " rake rust_on_background:start # start all queues"
76
+ say " rake rust_on_background:start[queue_name] # start specific queue"
77
+ say " rake rust_on_background:stop # stop all queues"
78
+ say " rake rust_on_background:stop[queue_name] # stop specific queue"
79
+ say " rake rust_on_background:restart"
80
+ say " rake rust_on_background:rebuild # stop, build, start"
81
+ say " rake rust_on_background:status"
82
+ say " rake rust_on_background:scheduler:start # start scheduler (one per Redis)"
83
+ say " rake rust_on_background:scheduler:stop # stop scheduler"
84
+ say ""
85
+ say "Environment-specific configs:"
86
+ say " config.development.toml, config.production.toml, config.test.toml"
87
+ say ""
88
+ end
89
+
90
+ private
91
+
92
+ def detect_database_adapter
93
+ config = database_config
94
+ config["adapter"] || config[:adapter] || "mysql2"
95
+ end
96
+
97
+ def database_config
98
+ db_config_path = Rails.root.join("config", "database.yml")
99
+ yaml_content = ERB.new(File.read(db_config_path)).result
100
+
101
+ config = if YAML.method(:safe_load).parameters.any? { |type, name| name == :aliases }
102
+ YAML.safe_load(yaml_content, aliases: true, permitted_classes: [Symbol])
103
+ else
104
+ YAML.safe_load(yaml_content, [Symbol], [], true)
105
+ end
106
+
107
+ env = Rails.env || "development"
108
+ config[env] || config["default"] || {}
109
+ end
110
+
111
+ def build_database_url
112
+ config = database_config
113
+
114
+ # Use url key if present
115
+ return config["url"] || config[:url] if config["url"] || config[:url]
116
+
117
+ url_adapter = database_feature_for(detect_database_adapter)
118
+ database = config["database"] || config[:database]
119
+ raise RustOnBackground::Error, "Missing database name" unless database
120
+
121
+ case url_adapter
122
+ when "sqlite"
123
+ "sqlite://#{database}"
124
+ when "mysql"
125
+ build_mysql_url(config, database)
126
+ when "postgres"
127
+ build_postgres_url(config, database)
128
+ else
129
+ raise RustOnBackground::Error, "Unsupported database adapter: #{url_adapter}"
130
+ end
131
+ end
132
+
133
+ def build_mysql_url(config, database)
134
+ socket = config["socket"] || config[:socket]
135
+ host = config["host"] || config[:host] || (socket ? "localhost" : nil)
136
+ raise RustOnBackground::Error, "Missing database host (and no socket configured)" unless host
137
+
138
+ username = config["username"] || config[:username] || "root"
139
+ password = config["password"] || config[:password]
140
+ port = config["port"] || config[:port]
141
+
142
+ auth = password ? "#{username}:#{password}" : username
143
+ port_str = port ? ":#{port}" : ""
144
+ socket_str = socket ? "?socket=#{socket}" : ""
145
+
146
+ "mysql://#{auth}@#{host}#{port_str}/#{database}#{socket_str}"
147
+ end
148
+
149
+ def build_postgres_url(config, database)
150
+ host = config["host"] || config[:host] || "localhost"
151
+ username = config["username"] || config[:username] || config["user"] || config[:user] || "postgres"
152
+ password = config["password"] || config[:password]
153
+ port = config["port"] || config[:port] || 5432
154
+
155
+ auth = password ? "#{username}:#{password}" : username
156
+
157
+ "postgres://#{auth}@#{host}:#{port}/#{database}"
158
+ end
159
+
160
+ def database_feature_for(adapter)
161
+ case adapter
162
+ when "mysql2", "mysql", "trilogy"
163
+ "mysql"
164
+ when "postgresql", "postgres", "postgis"
165
+ "postgres"
166
+ when "sqlite3", "sqlite"
167
+ "sqlite"
168
+ else
169
+ "mysql"
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "rust_on_background"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ # Core dependencies
8
+ tokio = { version = "1", features = ["full"] }
9
+ redis = { version = "0.25", features = ["tokio-comp"] }
10
+ serde = { version = "1", features = ["derive"] }
11
+ serde_json = "1"
12
+ toml = "0.8"
13
+ sqlx = { version = "0.7", features = ["runtime-tokio", "<%= @db_feature %>"] }
14
+ thiserror = "1"
15
+ tracing = "0.1"
16
+ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17
+ clap = { version = "4", features = ["derive"] }
18
+
19
+ # Custom dependencies below:
20
+ # example: calamine = "0.24"
@@ -0,0 +1,40 @@
1
+ # !!! Remember to create config.production.toml for production !!!
2
+
3
+ # Database connection (used by jobs that need DB access)
4
+ # Examples:
5
+ # url = "mysql://user:pass@127.0.0.1/myapp"
6
+ # url = "postgres://user:pass@127.0.0.1:5432/myapp"
7
+ # url = "sqlite://db/production.sqlite3"
8
+ [database]
9
+ url = "<%= @database_url %>"
10
+
11
+ # Redis connection (job queue)
12
+ # Examples:
13
+ # url = "redis://127.0.0.1:6379"
14
+ # url = "redis://:password@127.0.0.1:6379/0"
15
+ [redis]
16
+ url = "<%= @redis_url || "redis://127.0.0.1:6379" %>"
17
+
18
+ # Logging configuration
19
+ # path: relative to Rails root, or absolute path starting with /
20
+ # Leave empty to disable logging: path = ""
21
+ [logging]
22
+ path = "log/<%= Rails.env %>.log"
23
+
24
+ # Queue definitions
25
+ # name: queue name (matches RustOnBackground.perform(..., queue: "name"))
26
+ # workers: number of worker threads for this queue
27
+ [[queues]]
28
+ name = "default"
29
+ workers = 4
30
+
31
+ # [[queues]]
32
+ # name = "emails"
33
+ # workers = 2
34
+
35
+ # Scheduler configuration
36
+ # The scheduler moves jobs from the schedule to their target queues when due
37
+ # Run only ONE scheduler instance per Redis database
38
+ # [scheduler]
39
+ # poll_interval_ms = 1000
40
+ # batch_size = 100
@@ -0,0 +1,20 @@
1
+ // This file is like a Rails controller for background jobs
2
+ // This file routes incoming jobs to their handlers.
3
+ //
4
+ // When you run "rails g rust_on_background:job my_job", it automatically:
5
+ // 1. Creates a new job file: jobs/my_job.rs
6
+ // 2. Adds "pub mod my_job;" below
7
+ // 3. Adds a match case for this job in the dispatch function
8
+ // Check readMe for more please.
9
+
10
+ use crate::worker::{Job, JobResult};
11
+
12
+ #[allow(unused_variables)]
13
+ pub async fn dispatch(job: &Job, database_url: &str) -> JobResult {
14
+ match job.job.as_str() {
15
+ unknown => {
16
+ tracing::warn!("Unknown job type: {}", unknown);
17
+ JobResult::Failed(format!("Unknown job type: {}", unknown))
18
+ }
19
+ }
20
+ }