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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +338 -0
- data/Rakefile +4 -0
- data/lib/generators/install/install_generator.rb +174 -0
- data/lib/generators/install/templates/Cargo.toml.erb +20 -0
- data/lib/generators/install/templates/config.toml.erb +40 -0
- data/lib/generators/install/templates/jobs/mod.rs +20 -0
- data/lib/generators/install/templates/main.rs +144 -0
- data/lib/generators/install/templates/rust_on_background.rake +215 -0
- data/lib/generators/install/templates/scheduler.rs +135 -0
- data/lib/generators/install/templates/worker.rs +140 -0
- data/lib/generators/job/job_generator.rb +141 -0
- data/lib/generators/job/templates/job.rs.erb +37 -0
- data/lib/rust_on_background/railtie.rb +10 -0
- data/lib/rust_on_background/version.rb +5 -0
- data/lib/rust_on_background.rb +113 -0
- metadata +106 -0
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
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,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
|
+
}
|