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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RustOnBackground
|
|
6
|
+
module Generators
|
|
7
|
+
class JobGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
argument :job_name, type: :string, banner: "job_name"
|
|
10
|
+
argument :fields, type: :array, default: [], banner: "field:type field:type"
|
|
11
|
+
|
|
12
|
+
TYPE_MAPPINGS = {
|
|
13
|
+
# Ruby/Rails types
|
|
14
|
+
"string" => "String", "text" => "String", "citext" => "String",
|
|
15
|
+
"integer" => "i64", "int" => "i64", "i64" => "i64", "bigint" => "i64",
|
|
16
|
+
"i32" => "i32",
|
|
17
|
+
"float" => "f64", "decimal" => "f64", "f64" => "f64",
|
|
18
|
+
"boolean" => "bool", "bool" => "bool",
|
|
19
|
+
"array" => "Vec<serde_json::Value>", "vec" => "Vec<serde_json::Value>",
|
|
20
|
+
"set" => "Vec<serde_json::Value>",
|
|
21
|
+
"hash" => "serde_json::Value", "object" => "serde_json::Value",
|
|
22
|
+
"json" => "serde_json::Value", "jsonb" => "serde_json::Value",
|
|
23
|
+
"date" => "String", "datetime" => "String", "time" => "String", "timestamp" => "String",
|
|
24
|
+
"uuid" => "String", "binary" => "Vec<u8>", "blob" => "Vec<u8>"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
desc "Creates a new Rust background job"
|
|
28
|
+
|
|
29
|
+
def create_job_file
|
|
30
|
+
@args = parsed_attributes
|
|
31
|
+
template "job.rs.erb", "app/jobs/rust/src/jobs/#{file_name}.rs"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_module_declaration
|
|
35
|
+
mod_file = "app/jobs/rust/src/jobs/mod.rs"
|
|
36
|
+
|
|
37
|
+
inject_into_file mod_file, after: "use crate::worker::{Job, JobResult};\n" do
|
|
38
|
+
"pub mod #{file_name};\n"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_dispatch_route
|
|
43
|
+
mod_file = "app/jobs/rust/src/jobs/mod.rs"
|
|
44
|
+
content = File.read(mod_file)
|
|
45
|
+
|
|
46
|
+
new_content = content.sub(/^([ \t]*)unknown\s*=>/m) do |_match|
|
|
47
|
+
indent = $1
|
|
48
|
+
"#{indent}\"#{file_name}\" => {\n" \
|
|
49
|
+
"#{indent} #{file_name}::run(&job.args, database_url).await\n" \
|
|
50
|
+
"#{indent}}\n\n" \
|
|
51
|
+
"#{indent}unknown =>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
File.write(mod_file, new_content)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def show_post_generate_message
|
|
58
|
+
say ""
|
|
59
|
+
say "Job '#{file_name}' created!", :green
|
|
60
|
+
say ""
|
|
61
|
+
say "Files:"
|
|
62
|
+
say " app/jobs/rust/src/jobs/#{file_name}.rs"
|
|
63
|
+
say ""
|
|
64
|
+
say "Usage in Rails:"
|
|
65
|
+
say " RustOnBackground.perform(\"#{file_name}\"#{usage_example})"
|
|
66
|
+
say ""
|
|
67
|
+
say "Next steps:"
|
|
68
|
+
say " 1. Edit the job file to add your logic"
|
|
69
|
+
say " 2. Run: cd app/jobs/rust && cargo build"
|
|
70
|
+
say ""
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def file_name
|
|
76
|
+
job_name.underscore
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parsed_attributes
|
|
80
|
+
fields.map do |attr|
|
|
81
|
+
name, type = attr.split(":")
|
|
82
|
+
type ||= "string"
|
|
83
|
+
|
|
84
|
+
if model_type?(type)
|
|
85
|
+
{ name: name, rust_type: type, ruby_type: type, is_model: true }
|
|
86
|
+
else
|
|
87
|
+
{ name: name, rust_type: to_rust_type(type), ruby_type: type, is_model: false }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def model_type?(type)
|
|
93
|
+
type.match?(/\A[A-Z]/)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def model_attributes
|
|
97
|
+
@args.select { |a| a[:is_model] }.map { |a| a[:ruby_type] }.uniq
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def model_columns(model_name)
|
|
101
|
+
model_class = model_name.constantize
|
|
102
|
+
model_class.columns.map do |col|
|
|
103
|
+
{
|
|
104
|
+
name: col.name,
|
|
105
|
+
rust_type: to_rust_type(col.type, nullable: col.null)
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
rescue NameError
|
|
109
|
+
say "Warning: Model #{model_name} not found, using empty struct", :yellow
|
|
110
|
+
[{ name: "id", rust_type: "i64" }]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_rust_type(type, nullable: false)
|
|
114
|
+
key = type.to_s.downcase
|
|
115
|
+
base_type = TYPE_MAPPINGS[key] || "serde_json::Value"
|
|
116
|
+
nullable ? "Option<#{base_type}>" : base_type
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def usage_example
|
|
120
|
+
return "" if @args.empty?
|
|
121
|
+
|
|
122
|
+
examples = @args.map do |arg|
|
|
123
|
+
value = if arg[:is_model]
|
|
124
|
+
"#{arg[:ruby_type]}.first"
|
|
125
|
+
else
|
|
126
|
+
case arg[:ruby_type]
|
|
127
|
+
when "string", "text" then "\"...\""
|
|
128
|
+
when "integer", "int", "i64", "i32" then "123"
|
|
129
|
+
when "float", "decimal", "f64" then "1.5"
|
|
130
|
+
when "boolean", "bool" then "true"
|
|
131
|
+
else "..."
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
"#{arg[:name]}: #{value}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
", " + examples.join(", ")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
use serde::Deserialize;
|
|
2
|
+
use crate::worker::JobResult;
|
|
3
|
+
<% model_attributes.each do |model_name| -%>
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Deserialize)]
|
|
6
|
+
struct <%= model_name %> {
|
|
7
|
+
<% model_columns(model_name).each do |col| -%>
|
|
8
|
+
<%= col[:name] %>: <%= col[:rust_type] %>,
|
|
9
|
+
<% end -%>
|
|
10
|
+
}
|
|
11
|
+
<% end -%>
|
|
12
|
+
<% if @args.any? %>
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Deserialize)]
|
|
15
|
+
struct Args {
|
|
16
|
+
<% @args.each do |arg| -%>
|
|
17
|
+
<%= arg[:name] %>: <%= arg[:rust_type] %>,
|
|
18
|
+
<% end -%>
|
|
19
|
+
}
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
pub async fn run(args: &serde_json::Value, _database_url: &str) -> JobResult {
|
|
23
|
+
<% if @args.any? -%>
|
|
24
|
+
let args: Args = match serde_json::from_value(args.clone()) {
|
|
25
|
+
Ok(a) => a,
|
|
26
|
+
Err(e) => return JobResult::Failed(format!("Failed to parse args: {}", e)),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
tracing::info!("<%= file_name %> job started: {:?}", args);
|
|
30
|
+
<% else -%>
|
|
31
|
+
tracing::info!("<%= file_name %> job started with args: {:?}", args);
|
|
32
|
+
<% end -%>
|
|
33
|
+
|
|
34
|
+
// TODO: Add your job logic here
|
|
35
|
+
|
|
36
|
+
JobResult::Success
|
|
37
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "redis"
|
|
5
|
+
require "toml-rb"
|
|
6
|
+
require_relative "rust_on_background/version"
|
|
7
|
+
require_relative "rust_on_background/railtie" if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
module RustOnBackground
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
SCHEDULE_KEY = "rust_on_background:schedule"
|
|
13
|
+
|
|
14
|
+
def self.config
|
|
15
|
+
@config ||= TomlRB.load_file(Rails.root.join("app/jobs/rust/config.#{Rails.env}.toml"))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.perform(job_name, queue: "default", retry_count: 3, **args)
|
|
19
|
+
payload = {
|
|
20
|
+
job: job_name.to_s,
|
|
21
|
+
args: serialize_args(args),
|
|
22
|
+
retry: retry_count,
|
|
23
|
+
created_at: Time.now.to_i
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
redis.lpush("queue:#{queue}", payload.to_json)
|
|
28
|
+
rescue Redis::BaseError => e
|
|
29
|
+
raise Error, "Failed to enqueue job: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.perform_at(time, job_name, queue: "default", retry_count: 3, **args)
|
|
34
|
+
timestamp = normalize_time(time)
|
|
35
|
+
return perform(job_name, queue: queue, retry_count: retry_count, **args) if timestamp <= Time.now.to_f
|
|
36
|
+
|
|
37
|
+
payload = {
|
|
38
|
+
job: job_name.to_s,
|
|
39
|
+
args: serialize_args(args),
|
|
40
|
+
retry: retry_count,
|
|
41
|
+
created_at: Time.now.to_i,
|
|
42
|
+
queue: queue,
|
|
43
|
+
scheduled_at: timestamp.to_i
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
redis.zadd(SCHEDULE_KEY, timestamp, payload.to_json)
|
|
48
|
+
rescue Redis::BaseError => e
|
|
49
|
+
raise Error, "Failed to schedule job: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.serialize_args(args)
|
|
54
|
+
args.transform_values { |v| serialize_value(v) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.normalize_time(time)
|
|
58
|
+
case time
|
|
59
|
+
when Time then time.to_f
|
|
60
|
+
when DateTime then time.to_time.to_f
|
|
61
|
+
when Integer, Float then time.to_f
|
|
62
|
+
else
|
|
63
|
+
if time.respond_to?(:to_time)
|
|
64
|
+
time.to_time.to_f
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, "Invalid time class: #{time.class}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.serialize_value(value)
|
|
72
|
+
case value
|
|
73
|
+
when nil, true, false, Integer, Float, String
|
|
74
|
+
value
|
|
75
|
+
when Symbol
|
|
76
|
+
value.to_s
|
|
77
|
+
when Time, DateTime
|
|
78
|
+
value.iso8601
|
|
79
|
+
when Date
|
|
80
|
+
value.to_s
|
|
81
|
+
when BigDecimal
|
|
82
|
+
value.to_f
|
|
83
|
+
when Set
|
|
84
|
+
value.to_a.map { |v| serialize_value(v) }
|
|
85
|
+
when Range
|
|
86
|
+
{ start: serialize_value(value.begin), end: serialize_value(value.end), exclude_end: value.exclude_end? }
|
|
87
|
+
when Array
|
|
88
|
+
value.map { |v| serialize_value(v) }
|
|
89
|
+
when Hash
|
|
90
|
+
value.transform_keys(&:to_s).transform_values { |v| serialize_value(v) }
|
|
91
|
+
else
|
|
92
|
+
if defined?(ActiveRecord::Base) && value.is_a?(ActiveRecord::Base)
|
|
93
|
+
value.as_json
|
|
94
|
+
elsif defined?(ActiveRecord::Relation) && value.is_a?(ActiveRecord::Relation)
|
|
95
|
+
value.as_json
|
|
96
|
+
elsif value.respond_to?(:as_json)
|
|
97
|
+
value.as_json
|
|
98
|
+
elsif value.respond_to?(:to_h)
|
|
99
|
+
serialize_value(value.to_h)
|
|
100
|
+
else
|
|
101
|
+
value.to_s
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.redis
|
|
107
|
+
@redis ||= Redis.new(url: config.dig("redis", "url") || "redis://127.0.0.1:6379")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.redis=(client)
|
|
111
|
+
@redis = client
|
|
112
|
+
end
|
|
113
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rust_on_background
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- QuarkXZ
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: redis
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '4.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '4.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: toml-rb
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
description: A Rails gem that generates a Rust worker for processing background jobs
|
|
56
|
+
from Redis
|
|
57
|
+
email:
|
|
58
|
+
- quarkXZ@proton.me
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- LICENSE.txt
|
|
65
|
+
- README.md
|
|
66
|
+
- Rakefile
|
|
67
|
+
- lib/generators/install/install_generator.rb
|
|
68
|
+
- lib/generators/install/templates/Cargo.toml.erb
|
|
69
|
+
- lib/generators/install/templates/config.toml.erb
|
|
70
|
+
- lib/generators/install/templates/jobs/mod.rs
|
|
71
|
+
- lib/generators/install/templates/main.rs
|
|
72
|
+
- lib/generators/install/templates/rust_on_background.rake
|
|
73
|
+
- lib/generators/install/templates/scheduler.rs
|
|
74
|
+
- lib/generators/install/templates/worker.rs
|
|
75
|
+
- lib/generators/job/job_generator.rb
|
|
76
|
+
- lib/generators/job/templates/job.rs.erb
|
|
77
|
+
- lib/rust_on_background.rb
|
|
78
|
+
- lib/rust_on_background/railtie.rb
|
|
79
|
+
- lib/rust_on_background/version.rb
|
|
80
|
+
homepage: https://github.com/QuarkOnRails/rust_on_background
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://github.com/QuarkOnRails/rust_on_background
|
|
85
|
+
source_code_uri: https://github.com/QuarkOnRails/rust_on_background
|
|
86
|
+
changelog_uri: https://github.com/QuarkOnRails/rust_on_background/blob/main/CHANGELOG.md
|
|
87
|
+
post_install_message:
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 2.5.0
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 3.3.26
|
|
103
|
+
signing_key:
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Run background jobs in Rust for Rails applications
|
|
106
|
+
test_files: []
|