desiru 0.1.0 → 0.2.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 +4 -4
- data/.claude/settings.local.json +11 -0
- data/.env.example +34 -0
- data/.rubocop.yml +7 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +73 -0
- data/CLAUDE.local.md +3 -0
- data/CLAUDE.md +10 -1
- data/Gemfile +21 -2
- data/Gemfile.lock +88 -13
- data/README.md +301 -2
- data/Rakefile +1 -0
- data/db/migrations/001_create_initial_tables.rb +96 -0
- data/db/migrations/002_create_job_results.rb +39 -0
- data/desiru-development-swarm.yml +185 -0
- data/desiru.db +0 -0
- data/desiru.gemspec +2 -5
- data/docs/background_processing_roadmap.md +87 -0
- data/docs/job_scheduling.md +167 -0
- data/dspy-analysis-swarm.yml +60 -0
- data/dspy-feature-analysis.md +121 -0
- data/examples/README.md +69 -0
- data/examples/api_with_persistence.rb +122 -0
- data/examples/assertions_example.rb +232 -0
- data/examples/async_processing.rb +2 -0
- data/examples/few_shot_learning.rb +1 -2
- data/examples/graphql_api.rb +4 -2
- data/examples/graphql_integration.rb +3 -3
- data/examples/graphql_optimization_summary.md +143 -0
- data/examples/graphql_performance_benchmark.rb +247 -0
- data/examples/persistence_example.rb +102 -0
- data/examples/react_agent.rb +203 -0
- data/examples/rest_api.rb +173 -0
- data/examples/rest_api_advanced.rb +333 -0
- data/examples/scheduled_job_example.rb +116 -0
- data/examples/simple_qa.rb +1 -2
- data/examples/sinatra_api.rb +109 -0
- data/examples/typed_signatures.rb +1 -2
- data/graphql_optimization_summary.md +53 -0
- data/lib/desiru/api/grape_integration.rb +284 -0
- data/lib/desiru/api/persistence_middleware.rb +148 -0
- data/lib/desiru/api/sinatra_integration.rb +217 -0
- data/lib/desiru/api.rb +42 -0
- data/lib/desiru/assertions.rb +74 -0
- data/lib/desiru/async_status.rb +65 -0
- data/lib/desiru/cache.rb +1 -1
- data/lib/desiru/configuration.rb +2 -1
- data/lib/desiru/core/compiler.rb +231 -0
- data/lib/desiru/core/example.rb +96 -0
- data/lib/desiru/core/prediction.rb +108 -0
- data/lib/desiru/core/trace.rb +330 -0
- data/lib/desiru/core/traceable.rb +61 -0
- data/lib/desiru/core.rb +12 -0
- data/lib/desiru/errors.rb +160 -0
- data/lib/desiru/field.rb +17 -14
- data/lib/desiru/graphql/batch_loader.rb +85 -0
- data/lib/desiru/graphql/data_loader.rb +242 -75
- data/lib/desiru/graphql/enum_builder.rb +75 -0
- data/lib/desiru/graphql/executor.rb +37 -4
- data/lib/desiru/graphql/schema_generator.rb +62 -158
- data/lib/desiru/graphql/type_builder.rb +138 -0
- data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
- data/lib/desiru/jobs/async_predict.rb +1 -1
- data/lib/desiru/jobs/base.rb +67 -0
- data/lib/desiru/jobs/batch_processor.rb +6 -6
- data/lib/desiru/jobs/retriable.rb +119 -0
- data/lib/desiru/jobs/retry_strategies.rb +169 -0
- data/lib/desiru/jobs/scheduler.rb +219 -0
- data/lib/desiru/jobs/webhook_notifier.rb +242 -0
- data/lib/desiru/models/anthropic.rb +164 -0
- data/lib/desiru/models/base.rb +37 -3
- data/lib/desiru/models/open_ai.rb +151 -0
- data/lib/desiru/models/open_router.rb +161 -0
- data/lib/desiru/module.rb +67 -9
- data/lib/desiru/modules/best_of_n.rb +306 -0
- data/lib/desiru/modules/chain_of_thought.rb +3 -3
- data/lib/desiru/modules/majority.rb +51 -0
- data/lib/desiru/modules/multi_chain_comparison.rb +256 -0
- data/lib/desiru/modules/predict.rb +15 -1
- data/lib/desiru/modules/program_of_thought.rb +338 -0
- data/lib/desiru/modules/react.rb +273 -0
- data/lib/desiru/modules/retrieve.rb +4 -2
- data/lib/desiru/optimizers/base.rb +32 -4
- data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
- data/lib/desiru/optimizers/copro.rb +268 -0
- data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
- data/lib/desiru/optimizers/mipro_v2.rb +889 -0
- data/lib/desiru/persistence/database.rb +71 -0
- data/lib/desiru/persistence/models/api_request.rb +38 -0
- data/lib/desiru/persistence/models/job_result.rb +138 -0
- data/lib/desiru/persistence/models/module_execution.rb +37 -0
- data/lib/desiru/persistence/models/optimization_result.rb +28 -0
- data/lib/desiru/persistence/models/training_example.rb +25 -0
- data/lib/desiru/persistence/models.rb +11 -0
- data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
- data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
- data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
- data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
- data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
- data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
- data/lib/desiru/persistence/repository.rb +29 -0
- data/lib/desiru/persistence/setup.rb +77 -0
- data/lib/desiru/persistence.rb +49 -0
- data/lib/desiru/registry.rb +3 -5
- data/lib/desiru/signature.rb +91 -24
- data/lib/desiru/version.rb +1 -1
- data/lib/desiru.rb +33 -8
- data/missing-features-analysis.md +192 -0
- metadata +75 -45
- data/lib/desiru/models/raix_adapter.rb +0 -210
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
module Desiru
|
|
7
|
+
module Persistence
|
|
8
|
+
# Database connection and migration management
|
|
9
|
+
module Database
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :connection
|
|
12
|
+
|
|
13
|
+
def connect(database_url = nil)
|
|
14
|
+
url = database_url || Persistence.database_url
|
|
15
|
+
|
|
16
|
+
@connection = Sequel.connect(
|
|
17
|
+
url,
|
|
18
|
+
logger: logger,
|
|
19
|
+
max_connections: max_connections
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Enable foreign keys for SQLite
|
|
23
|
+
@connection.run('PRAGMA foreign_keys = ON') if sqlite?
|
|
24
|
+
|
|
25
|
+
@connection
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disconnect
|
|
29
|
+
@connection&.disconnect
|
|
30
|
+
@connection = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def migrate!
|
|
34
|
+
raise 'Not connected to database' unless @connection
|
|
35
|
+
|
|
36
|
+
Sequel.extension :migration
|
|
37
|
+
migrations_path = File.expand_path('../../../db/migrations', __dir__)
|
|
38
|
+
Sequel::Migrator.run(@connection, migrations_path)
|
|
39
|
+
|
|
40
|
+
# Initialize persistence layer after migrations
|
|
41
|
+
require_relative 'setup'
|
|
42
|
+
Setup.initialize!(@connection)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def transaction(&)
|
|
46
|
+
raise 'Not connected to database' unless @connection
|
|
47
|
+
|
|
48
|
+
@connection.transaction(&)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def logger
|
|
54
|
+
return nil unless ENV['DESIRU_DEBUG'] || ENV['DEBUG']
|
|
55
|
+
|
|
56
|
+
Logger.new($stdout).tap do |logger|
|
|
57
|
+
logger.level = Logger::INFO
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def max_connections
|
|
62
|
+
ENV['DESIRU_DB_MAX_CONNECTIONS']&.to_i || 10
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sqlite?
|
|
66
|
+
@connection&.adapter_scheme == :sqlite
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Models
|
|
6
|
+
# Tracks REST API requests
|
|
7
|
+
class ApiRequest < Base
|
|
8
|
+
set_dataset :api_requests
|
|
9
|
+
one_to_many :module_executions
|
|
10
|
+
|
|
11
|
+
json_column :headers
|
|
12
|
+
json_column :params
|
|
13
|
+
json_column :response_body
|
|
14
|
+
|
|
15
|
+
def validate
|
|
16
|
+
super
|
|
17
|
+
# Validate method column separately due to name conflict with Ruby's method method
|
|
18
|
+
if self[:method].nil? || self[:method].to_s.empty?
|
|
19
|
+
errors.add(:method, 'is required')
|
|
20
|
+
elsif !%w[GET POST PUT PATCH DELETE].include?(self[:method])
|
|
21
|
+
errors.add(:method, 'must be GET, POST, PUT, PATCH, or DELETE')
|
|
22
|
+
end
|
|
23
|
+
validates_presence %i[path status_code]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def success?
|
|
27
|
+
status_code >= 200 && status_code < 300
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def duration_ms
|
|
31
|
+
return nil unless response_time
|
|
32
|
+
|
|
33
|
+
(response_time * 1000).round
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Models
|
|
6
|
+
# Model for storing background job results
|
|
7
|
+
class JobResult < Base
|
|
8
|
+
set_dataset :job_results
|
|
9
|
+
|
|
10
|
+
# Status constants
|
|
11
|
+
STATUS_PENDING = 'pending'
|
|
12
|
+
STATUS_PROCESSING = 'processing'
|
|
13
|
+
STATUS_COMPLETED = 'completed'
|
|
14
|
+
STATUS_FAILED = 'failed'
|
|
15
|
+
|
|
16
|
+
# Validations
|
|
17
|
+
def validate
|
|
18
|
+
super
|
|
19
|
+
validates_presence %i[job_id job_class queue status enqueued_at]
|
|
20
|
+
validates_unique :job_id if db&.table_exists?(:job_results)
|
|
21
|
+
validates_includes %w[pending processing completed failed], :status
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Scopes
|
|
25
|
+
dataset_module do
|
|
26
|
+
def pending
|
|
27
|
+
where(status: STATUS_PENDING)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def processing
|
|
31
|
+
where(status: STATUS_PROCESSING)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def completed
|
|
35
|
+
where(status: STATUS_COMPLETED)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def failed
|
|
39
|
+
where(status: STATUS_FAILED)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def expired
|
|
43
|
+
where { expires_at < Time.now }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def active
|
|
47
|
+
where { (expires_at > Time.now) | (expires_at =~ nil) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def by_job_class(job_class)
|
|
51
|
+
where(job_class: job_class)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def recent(limit = 10)
|
|
55
|
+
order(Sequel.desc(:created_at)).limit(limit)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Instance methods
|
|
60
|
+
def pending?
|
|
61
|
+
status == STATUS_PENDING
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def processing?
|
|
65
|
+
status == STATUS_PROCESSING
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def completed?
|
|
69
|
+
status == STATUS_COMPLETED
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def failed?
|
|
73
|
+
status == STATUS_FAILED
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def expired?
|
|
77
|
+
expires_at && expires_at < Time.now
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def duration
|
|
81
|
+
return nil unless started_at && finished_at
|
|
82
|
+
|
|
83
|
+
finished_at - started_at
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# JSON field accessors
|
|
87
|
+
def inputs_data
|
|
88
|
+
return {} unless inputs
|
|
89
|
+
|
|
90
|
+
JSON.parse(inputs, symbolize_names: true)
|
|
91
|
+
rescue JSON::ParserError
|
|
92
|
+
{}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def result_data
|
|
96
|
+
return {} unless result
|
|
97
|
+
|
|
98
|
+
JSON.parse(result, symbolize_names: true)
|
|
99
|
+
rescue JSON::ParserError
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def mark_as_processing!
|
|
104
|
+
update(
|
|
105
|
+
status: STATUS_PROCESSING,
|
|
106
|
+
started_at: Time.now,
|
|
107
|
+
progress: 0
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mark_as_completed!(result_data, message: nil)
|
|
112
|
+
update(
|
|
113
|
+
status: STATUS_COMPLETED,
|
|
114
|
+
finished_at: Time.now,
|
|
115
|
+
progress: 100,
|
|
116
|
+
result: result_data.to_json,
|
|
117
|
+
message: message
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mark_as_failed!(error, backtrace: nil)
|
|
122
|
+
update(
|
|
123
|
+
status: STATUS_FAILED,
|
|
124
|
+
finished_at: Time.now,
|
|
125
|
+
error_message: error.to_s,
|
|
126
|
+
error_backtrace: backtrace&.join("\n")
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def update_progress(progress, message: nil)
|
|
131
|
+
updates = { progress: progress }
|
|
132
|
+
updates[:message] = message if message
|
|
133
|
+
update(updates)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Models
|
|
6
|
+
# Tracks module execution history
|
|
7
|
+
class ModuleExecution < Base
|
|
8
|
+
set_dataset :module_executions
|
|
9
|
+
many_to_one :api_request
|
|
10
|
+
|
|
11
|
+
json_column :inputs
|
|
12
|
+
json_column :outputs
|
|
13
|
+
json_column :metadata
|
|
14
|
+
|
|
15
|
+
def validate
|
|
16
|
+
super
|
|
17
|
+
validates_presence %i[module_name status started_at]
|
|
18
|
+
validates_includes %w[pending running completed failed], :status
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def duration
|
|
22
|
+
return nil unless started_at && finished_at
|
|
23
|
+
|
|
24
|
+
finished_at - started_at
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def success?
|
|
28
|
+
status == 'completed'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failed?
|
|
32
|
+
status == 'failed'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Models
|
|
6
|
+
# Stores optimization results and metrics
|
|
7
|
+
class OptimizationResult < Base
|
|
8
|
+
set_dataset :optimization_results
|
|
9
|
+
json_column :parameters
|
|
10
|
+
json_column :metrics
|
|
11
|
+
json_column :best_prompts
|
|
12
|
+
|
|
13
|
+
def validate
|
|
14
|
+
super
|
|
15
|
+
validates_presence %i[module_name optimizer_type score]
|
|
16
|
+
validates_numeric :score
|
|
17
|
+
validates_min_length 1, :training_size if training_size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def improvement_percentage
|
|
21
|
+
return nil unless baseline_score && score.positive?
|
|
22
|
+
|
|
23
|
+
((score - baseline_score) / baseline_score * 100).round(2)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Models
|
|
6
|
+
# Stores training examples for modules
|
|
7
|
+
class TrainingExample < Base
|
|
8
|
+
set_dataset :training_examples
|
|
9
|
+
json_column :inputs
|
|
10
|
+
json_column :expected_outputs
|
|
11
|
+
json_column :metadata
|
|
12
|
+
|
|
13
|
+
def validate
|
|
14
|
+
super
|
|
15
|
+
validates_presence %i[module_name inputs]
|
|
16
|
+
validates_includes %w[training validation test], :dataset_type if dataset_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def used?
|
|
20
|
+
used_count&.positive?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_repository'
|
|
4
|
+
|
|
5
|
+
module Desiru
|
|
6
|
+
module Persistence
|
|
7
|
+
module Repositories
|
|
8
|
+
# Repository for API request records
|
|
9
|
+
class ApiRequestRepository < BaseRepository
|
|
10
|
+
def initialize
|
|
11
|
+
super(Models::ApiRequest)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find_by_path(path)
|
|
15
|
+
dataset.where(path: path).all
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recent(limit = 10)
|
|
19
|
+
dataset
|
|
20
|
+
.order(Sequel.desc(:created_at))
|
|
21
|
+
.limit(limit)
|
|
22
|
+
.all
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def by_status_code_range(min, max)
|
|
26
|
+
dataset.where(status_code: min..max).all
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def successful
|
|
30
|
+
by_status_code_range(200, 299)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def failed
|
|
34
|
+
dataset.where { status_code >= 400 }.all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def average_response_time(path = nil)
|
|
38
|
+
scope = dataset
|
|
39
|
+
scope = scope.where(path: path) if path
|
|
40
|
+
scope = scope.exclude(response_time: nil)
|
|
41
|
+
|
|
42
|
+
avg = scope.avg(:response_time)
|
|
43
|
+
avg&.round(3)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def requests_per_minute(minutes_ago = 60)
|
|
47
|
+
since = Time.now - (minutes_ago * 60)
|
|
48
|
+
count = dataset.where { created_at >= since }.count
|
|
49
|
+
|
|
50
|
+
(count.to_f / minutes_ago).round(2)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def top_paths(limit = 10)
|
|
54
|
+
dataset
|
|
55
|
+
.group_and_count(:path)
|
|
56
|
+
.order(Sequel.desc(:count))
|
|
57
|
+
.limit(limit)
|
|
58
|
+
.map { |row| { path: row[:path], count: row[:count] } }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_from_rack_request(request, response)
|
|
62
|
+
create(
|
|
63
|
+
method: request.request_method,
|
|
64
|
+
path: request.path_info,
|
|
65
|
+
remote_ip: request.ip,
|
|
66
|
+
headers: extract_headers(request),
|
|
67
|
+
params: request.params,
|
|
68
|
+
status_code: response.status,
|
|
69
|
+
response_body: extract_response_body(response),
|
|
70
|
+
response_time: response.headers['X-Runtime']&.to_f
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def extract_headers(request)
|
|
77
|
+
headers = {}
|
|
78
|
+
request.each_header do |key, value|
|
|
79
|
+
next unless key.start_with?('HTTP_')
|
|
80
|
+
|
|
81
|
+
header_name = key.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
|
82
|
+
headers[header_name] = value
|
|
83
|
+
end
|
|
84
|
+
headers
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_response_body(response)
|
|
88
|
+
return nil unless response.body.respond_to?(:each)
|
|
89
|
+
|
|
90
|
+
body = response.body.map { |part| part }
|
|
91
|
+
body.join
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Desiru
|
|
4
|
+
module Persistence
|
|
5
|
+
module Repositories
|
|
6
|
+
# Base repository with common CRUD operations
|
|
7
|
+
class BaseRepository
|
|
8
|
+
attr_reader :model_class
|
|
9
|
+
|
|
10
|
+
def initialize(model_class)
|
|
11
|
+
@model_class = model_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all
|
|
15
|
+
dataset.all
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def find(id)
|
|
19
|
+
dataset.first(id: id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_by(conditions)
|
|
23
|
+
dataset.where(conditions).first
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def where(conditions)
|
|
27
|
+
dataset.where(conditions).all
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create(attributes)
|
|
31
|
+
model_class.create(attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update(id, attributes)
|
|
35
|
+
record = find(id)
|
|
36
|
+
return nil unless record
|
|
37
|
+
|
|
38
|
+
record.update(attributes)
|
|
39
|
+
record
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete?(id)
|
|
43
|
+
record = find(id)
|
|
44
|
+
return false unless record
|
|
45
|
+
|
|
46
|
+
record.destroy
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def count
|
|
51
|
+
dataset.count
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def exists?(conditions)
|
|
55
|
+
dataset.where(conditions).any?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def paginate(page: 1, per_page: 20)
|
|
59
|
+
dataset
|
|
60
|
+
.limit(per_page)
|
|
61
|
+
.offset((page - 1) * per_page)
|
|
62
|
+
.all
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
|
|
67
|
+
def dataset
|
|
68
|
+
model_class.dataset
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def transaction(&)
|
|
72
|
+
Database.transaction(&)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_repository'
|
|
4
|
+
|
|
5
|
+
module Desiru
|
|
6
|
+
module Persistence
|
|
7
|
+
module Repositories
|
|
8
|
+
# Repository for job result persistence
|
|
9
|
+
class JobResultRepository < BaseRepository
|
|
10
|
+
def initialize
|
|
11
|
+
super(Models::JobResult)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_for_job(job_id, job_class, queue, inputs: nil, expires_at: nil)
|
|
15
|
+
create(
|
|
16
|
+
job_id: job_id,
|
|
17
|
+
job_class: job_class,
|
|
18
|
+
queue: queue,
|
|
19
|
+
status: Models::JobResult::STATUS_PENDING,
|
|
20
|
+
inputs: inputs&.to_json,
|
|
21
|
+
enqueued_at: Time.now,
|
|
22
|
+
expires_at: expires_at
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_by_job_id(job_id)
|
|
27
|
+
find_by(job_id: job_id)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mark_processing(job_id)
|
|
31
|
+
job_result = find_by_job_id(job_id)
|
|
32
|
+
return nil unless job_result
|
|
33
|
+
|
|
34
|
+
job_result.mark_as_processing!
|
|
35
|
+
job_result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_completed(job_id, result, message: nil)
|
|
39
|
+
job_result = find_by_job_id(job_id)
|
|
40
|
+
return nil unless job_result
|
|
41
|
+
|
|
42
|
+
job_result.mark_as_completed!(result, message: message)
|
|
43
|
+
job_result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mark_failed(job_id, error, backtrace: nil, increment_retry: true)
|
|
47
|
+
job_result = find_by_job_id(job_id)
|
|
48
|
+
return nil unless job_result
|
|
49
|
+
|
|
50
|
+
updates = {
|
|
51
|
+
status: Models::JobResult::STATUS_FAILED,
|
|
52
|
+
finished_at: Time.now,
|
|
53
|
+
error_message: error.to_s,
|
|
54
|
+
error_backtrace: backtrace&.join("\n")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updates[:retry_count] = job_result.retry_count + 1 if increment_retry
|
|
58
|
+
|
|
59
|
+
job_result.update(updates)
|
|
60
|
+
job_result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def update_progress(job_id, progress, message: nil)
|
|
64
|
+
job_result = find_by_job_id(job_id)
|
|
65
|
+
return nil unless job_result
|
|
66
|
+
|
|
67
|
+
job_result.update_progress(progress, message: message)
|
|
68
|
+
job_result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cleanup_expired
|
|
72
|
+
dataset.expired.delete
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def recent_by_class(job_class, limit: 10)
|
|
76
|
+
dataset.by_job_class(job_class).recent(limit).all
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def statistics(job_class: nil, since: nil)
|
|
80
|
+
scope = dataset
|
|
81
|
+
scope = scope.by_job_class(job_class) if job_class
|
|
82
|
+
scope = scope.where { created_at >= since } if since
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
total: scope.count,
|
|
86
|
+
pending: scope.pending.count,
|
|
87
|
+
processing: scope.processing.count,
|
|
88
|
+
completed: scope.completed.count,
|
|
89
|
+
failed: scope.failed.count,
|
|
90
|
+
average_duration: calculate_average_duration(scope)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def calculate_average_duration(dataset)
|
|
97
|
+
completed = dataset.completed.where(Sequel.~(started_at: nil)).where(Sequel.~(finished_at: nil))
|
|
98
|
+
return 0 if completed.empty?
|
|
99
|
+
|
|
100
|
+
total_duration = 0
|
|
101
|
+
count = 0
|
|
102
|
+
|
|
103
|
+
completed.each do |job|
|
|
104
|
+
next unless job.started_at && job.finished_at
|
|
105
|
+
|
|
106
|
+
duration = job.finished_at - job.started_at
|
|
107
|
+
total_duration += duration
|
|
108
|
+
count += 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
count.positive? ? total_duration / count : 0
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|