desiru 0.1.0 → 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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +34 -0
  3. data/.rubocop.yml +7 -4
  4. data/.ruby-version +1 -0
  5. data/CLAUDE.md +4 -0
  6. data/Gemfile +21 -2
  7. data/Gemfile.lock +87 -12
  8. data/README.md +295 -2
  9. data/Rakefile +1 -0
  10. data/db/migrations/001_create_initial_tables.rb +96 -0
  11. data/db/migrations/002_create_job_results.rb +39 -0
  12. data/desiru.db +0 -0
  13. data/desiru.gemspec +2 -5
  14. data/docs/background_processing_roadmap.md +87 -0
  15. data/docs/job_scheduling.md +167 -0
  16. data/dspy-analysis-swarm.yml +60 -0
  17. data/dspy-feature-analysis.md +121 -0
  18. data/examples/README.md +69 -0
  19. data/examples/api_with_persistence.rb +122 -0
  20. data/examples/assertions_example.rb +232 -0
  21. data/examples/async_processing.rb +2 -0
  22. data/examples/few_shot_learning.rb +1 -2
  23. data/examples/graphql_api.rb +4 -2
  24. data/examples/graphql_integration.rb +3 -3
  25. data/examples/graphql_optimization_summary.md +143 -0
  26. data/examples/graphql_performance_benchmark.rb +247 -0
  27. data/examples/persistence_example.rb +102 -0
  28. data/examples/react_agent.rb +203 -0
  29. data/examples/rest_api.rb +173 -0
  30. data/examples/rest_api_advanced.rb +333 -0
  31. data/examples/scheduled_job_example.rb +116 -0
  32. data/examples/simple_qa.rb +1 -2
  33. data/examples/sinatra_api.rb +109 -0
  34. data/examples/typed_signatures.rb +1 -2
  35. data/graphql_optimization_summary.md +53 -0
  36. data/lib/desiru/api/grape_integration.rb +284 -0
  37. data/lib/desiru/api/persistence_middleware.rb +148 -0
  38. data/lib/desiru/api/sinatra_integration.rb +217 -0
  39. data/lib/desiru/api.rb +42 -0
  40. data/lib/desiru/assertions.rb +74 -0
  41. data/lib/desiru/async_status.rb +65 -0
  42. data/lib/desiru/cache.rb +1 -1
  43. data/lib/desiru/configuration.rb +2 -1
  44. data/lib/desiru/errors.rb +160 -0
  45. data/lib/desiru/field.rb +17 -14
  46. data/lib/desiru/graphql/batch_loader.rb +85 -0
  47. data/lib/desiru/graphql/data_loader.rb +242 -75
  48. data/lib/desiru/graphql/enum_builder.rb +75 -0
  49. data/lib/desiru/graphql/executor.rb +37 -4
  50. data/lib/desiru/graphql/schema_generator.rb +62 -158
  51. data/lib/desiru/graphql/type_builder.rb +138 -0
  52. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  53. data/lib/desiru/jobs/async_predict.rb +1 -1
  54. data/lib/desiru/jobs/base.rb +67 -0
  55. data/lib/desiru/jobs/batch_processor.rb +6 -6
  56. data/lib/desiru/jobs/retriable.rb +119 -0
  57. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  58. data/lib/desiru/jobs/scheduler.rb +219 -0
  59. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  60. data/lib/desiru/models/anthropic.rb +164 -0
  61. data/lib/desiru/models/base.rb +37 -3
  62. data/lib/desiru/models/open_ai.rb +151 -0
  63. data/lib/desiru/models/open_router.rb +161 -0
  64. data/lib/desiru/module.rb +59 -9
  65. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  66. data/lib/desiru/modules/majority.rb +51 -0
  67. data/lib/desiru/modules/multi_chain_comparison.rb +204 -0
  68. data/lib/desiru/modules/predict.rb +8 -1
  69. data/lib/desiru/modules/program_of_thought.rb +139 -0
  70. data/lib/desiru/modules/react.rb +273 -0
  71. data/lib/desiru/modules/retrieve.rb +4 -2
  72. data/lib/desiru/optimizers/base.rb +2 -4
  73. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  74. data/lib/desiru/optimizers/copro.rb +268 -0
  75. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  76. data/lib/desiru/persistence/database.rb +71 -0
  77. data/lib/desiru/persistence/models/api_request.rb +38 -0
  78. data/lib/desiru/persistence/models/job_result.rb +138 -0
  79. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  80. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  81. data/lib/desiru/persistence/models/training_example.rb +25 -0
  82. data/lib/desiru/persistence/models.rb +11 -0
  83. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  84. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  85. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  86. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  87. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  88. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  89. data/lib/desiru/persistence/repository.rb +29 -0
  90. data/lib/desiru/persistence/setup.rb +77 -0
  91. data/lib/desiru/persistence.rb +49 -0
  92. data/lib/desiru/registry.rb +3 -5
  93. data/lib/desiru/signature.rb +91 -24
  94. data/lib/desiru/version.rb +1 -1
  95. data/lib/desiru.rb +23 -8
  96. data/missing-features-analysis.md +192 -0
  97. metadata +63 -45
  98. data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'graphql'
4
4
  require_relative 'data_loader'
5
+ require_relative 'batch_loader'
6
+ require_relative 'type_builder'
5
7
 
6
8
  module Desiru
7
9
  module GraphQL
@@ -12,11 +14,16 @@ module Desiru
12
14
  def initialize
13
15
  @signatures = {}
14
16
  @modules = {}
15
- @type_cache = {}
17
+ @type_cache = {} # Instance cache for schema-specific types
16
18
  @schema_class = nil
17
19
  @data_loader = DataLoader.new
18
20
  end
19
21
 
22
+ # Clear the global type cache (useful for testing or reloading)
23
+ def self.clear_type_cache!
24
+ TypeBuilder.clear_type_cache!
25
+ end
26
+
20
27
  # Register a signature with a name for GraphQL query/mutation
21
28
  def register_signature(name, signature)
22
29
  @signatures[name] = signature
@@ -36,47 +43,68 @@ module Desiru
36
43
 
37
44
  # Generate a GraphQL schema from registered signatures
38
45
  def generate_schema
39
- return @schema_class if @schema_class && @signatures.empty?
46
+ # Always rebuild if signatures have changed
47
+ return @schema_class if @schema_class && @last_signature_count == @signatures.size
40
48
 
49
+ @last_signature_count = @signatures.size
41
50
  query_class = build_query_type
42
51
 
43
52
  @schema_class = Class.new(::GraphQL::Schema) do
44
53
  query(query_class) if query_class
54
+
55
+ # Enable GraphQL's built-in dataloader
56
+ use ::GraphQL::Dataloader
57
+
58
+ # Enable lazy execution for batch loading
59
+ lazy_resolve(::GraphQL::Execution::Lazy, :value)
45
60
  end
46
61
 
47
62
  @schema_class
48
63
  end
49
64
 
65
+ def add_query_field(query_class, field_name, field_def)
66
+ # Add field directly without resolver class
67
+ query_class.field field_name, field_def[:type],
68
+ null: false,
69
+ description: field_def[:description] do
70
+ # Add arguments
71
+ field_def[:arguments].each do |arg_name, arg_def|
72
+ argument arg_name, arg_def[:type], required: arg_def[:required]
73
+ end
74
+ end
75
+
76
+ # Define the resolver method for this field
77
+ query_class.define_method field_name do |**args|
78
+ if field_def[:module_instance].respond_to?(:batch_forward)
79
+ # Get the dataloader for this request
80
+ dataloader = context.dataloader
81
+ # Load through the dataloader
82
+ dataloader.with(
83
+ Desiru::GraphQL::ModuleLoader,
84
+ field_name,
85
+ field_def[:modules]
86
+ ).load(args)
87
+ else
88
+ # Direct execution
89
+ field_def[:resolver].call(args, context)
90
+ end
91
+ end
92
+ end
93
+
50
94
  private
51
95
 
52
96
  def build_query_type
53
97
  return nil if @signatures.empty?
54
98
 
55
99
  query_fields = build_query_fields
100
+ query_class_builder = self
56
101
 
57
102
  Class.new(::GraphQL::Schema::Object) do
58
103
  graphql_name 'Query'
59
104
  description 'Desiru query operations'
60
105
 
61
106
  query_fields.each do |field_name, field_def|
62
- # Create a resolver class for each field
63
- resolver_class = Class.new(::GraphQL::Schema::Resolver) do
64
- # Set the return type
65
- type field_def[:type], null: false
66
-
67
- # Add arguments
68
- field_def[:arguments].each do |arg_name, arg_def|
69
- argument arg_name, arg_def[:type], required: arg_def[:required]
70
- end
71
-
72
- # Define resolve method
73
- define_method :resolve do |**args|
74
- field_def[:resolver].call(args)
75
- end
76
- end
77
-
78
- # Add field with resolver
79
- field field_name, resolver: resolver_class, description: field_def[:description]
107
+ query_class_builder.add_query_field(self, field_name, field_def)
80
108
  end
81
109
  end
82
110
  end
@@ -85,12 +113,12 @@ module Desiru
85
113
  fields = {}
86
114
 
87
115
  @signatures.each do |operation_name, signature|
88
- output_type = build_output_type(signature)
116
+ output_type = TypeBuilder.build_output_type(signature)
89
117
 
90
118
  arguments = {}
91
119
  signature.input_fields.each do |field_name, field|
92
120
  arguments[camelcase_field_name(field_name)] = {
93
- type: graphql_type_for_field(field),
121
+ type: TypeBuilder.graphql_type_for_field(field),
94
122
  required: !field.optional
95
123
  }
96
124
  end
@@ -99,7 +127,9 @@ module Desiru
99
127
  type: output_type,
100
128
  description: "Generated from signature: #{signature.raw_signature}",
101
129
  arguments: arguments,
102
- resolver: ->(args) { execute_signature(operation_name, signature, args) }
130
+ resolver: ->(args, context) { execute_signature(operation_name, signature, args, context) },
131
+ module_instance: @modules[operation_name],
132
+ modules: @modules
103
133
  }
104
134
  end
105
135
 
@@ -111,151 +141,25 @@ module Desiru
111
141
  nil
112
142
  end
113
143
 
114
- def build_output_type(signature)
115
- type_name = "Output#{signature.object_id}"
116
- return @type_cache[type_name] if @type_cache[type_name]
117
-
118
- output_field_defs = {}
119
- signature.output_fields.each do |field_name, field|
120
- output_field_defs[camelcase_field_name(field_name)] = {
121
- type: graphql_type_for_field(field),
122
- null: field.optional,
123
- description: field.description
124
- }
125
- end
126
-
127
- output_type = Class.new(::GraphQL::Schema::Object) do
128
- graphql_name type_name
129
- description 'Generated output type'
130
-
131
- output_field_defs.each do |field_name, field_def|
132
- field field_name, field_def[:type],
133
- null: field_def[:null],
134
- description: field_def[:description]
135
- end
136
- end
137
-
138
- @type_cache[type_name] = output_type
139
- end
140
-
141
- def graphql_type_for_field(field)
142
- base_type = case field.type
143
- when :string
144
- ::GraphQL::Types::String
145
- when :int, :integer
146
- ::GraphQL::Types::Int
147
- when :float
148
- ::GraphQL::Types::Float
149
- when :bool, :boolean
150
- ::GraphQL::Types::Boolean
151
- when :list
152
- # Handle list types
153
- element_type = graphql_type_for_element(field.element_type)
154
- [element_type]
155
- when :literal
156
- # Create enum type for literal values
157
- create_enum_type(field)
158
- else
159
- ::GraphQL::Types::String
160
- end
161
-
162
- if field.optional
163
- base_type
164
- else
165
- # Arrays are already wrapped, so handle them differently
166
- base_type.is_a?(Array) ? [base_type.first, { null: false }] : base_type.to_non_null_type
167
- end
168
- end
169
-
170
- def graphql_type_for_element(element_type)
171
- case element_type
172
- when Hash
173
- # Handle typed arrays like List[Literal['yes', 'no']]
174
- if element_type[:type] == :literal
175
- create_enum_type_from_values(element_type[:values])
176
- else
177
- ::GraphQL::Types::String
178
- end
179
- else
180
- # Simple types
181
- case element_type
182
- when :string then ::GraphQL::Types::String
183
- when :int, :integer then ::GraphQL::Types::Int
184
- when :float then ::GraphQL::Types::Float
185
- when :bool, :boolean then ::GraphQL::Types::Boolean
186
- else ::GraphQL::Types::String
187
- end
188
- end
189
- end
190
-
191
- def create_enum_type(field)
192
- enum_name = "#{field.name.to_s.capitalize}Enum"
193
- return @type_cache[enum_name] if @type_cache[enum_name]
194
-
195
- # Extract literal values from the field's validator
196
- values = extract_literal_values(field)
197
-
198
- enum_type = Class.new(::GraphQL::Schema::Enum) do
199
- graphql_name enum_name
200
- description "Enum for #{field.name}"
201
-
202
- values.each do |val|
203
- value val.upcase.gsub(/[^A-Z0-9_]/, '_'), value: val
204
- end
205
- end
206
-
207
- @type_cache[enum_name] = enum_type
208
- end
209
-
210
- def create_enum_type_from_values(values)
211
- enum_name = "Literal#{values.map(&:capitalize).join}Enum"
212
- return @type_cache[enum_name] if @type_cache[enum_name]
213
-
214
- enum_type = Class.new(::GraphQL::Schema::Enum) do
215
- graphql_name enum_name
216
-
217
- values.each do |val|
218
- value val.upcase.gsub(/[^A-Z0-9_]/, '_'), value: val
219
- end
220
- end
221
-
222
- @type_cache[enum_name] = enum_type
223
- end
224
-
225
- def extract_literal_values(field)
226
- # Try to extract values from the field's validator
227
- if field.respond_to?(:validator) && field.validator.respond_to?(:instance_variable_get)
228
- field.validator.instance_variable_get(:@values) || []
229
- elsif field.respond_to?(:element_type) && field.element_type.is_a?(Hash)
230
- field.element_type[:values] || []
231
- else
232
- []
233
- end
234
- end
235
-
236
- def execute_signature(operation_name, signature, args)
144
+ def execute_signature(operation_name, signature, args, context = {})
237
145
  # Convert GraphQL arguments from camelCase to snake_case
238
146
  inputs = transform_graphql_args(args)
239
147
 
148
+ # Get data loader from context if available
149
+ context[:data_loader] || @data_loader
150
+
240
151
  # Check if we have a registered module for this operation
241
152
  if @modules[operation_name]
242
- # Use DataLoader for batch optimization
243
- loader = @data_loader.for(@modules[operation_name].class)
244
- promise = loader.load(inputs)
245
-
246
- # In a real GraphQL implementation, this would be handled by the executor
247
- # For now, we'll resolve immediately
248
- result = promise.value
153
+ module_instance = @modules[operation_name]
249
154
 
250
- # Transform module result to GraphQL response format
251
- transform_module_result(result)
155
+ # Direct execution - batching will be handled by the executor
252
156
  else
253
157
  # Fallback: create a module instance on the fly
254
158
  module_class = infer_module_class(signature)
255
159
  module_instance = module_class.new(signature)
256
- result = module_instance.call(inputs)
257
- transform_module_result(result)
258
160
  end
161
+ result = module_instance.call(inputs)
162
+ transform_module_result(result)
259
163
  end
260
164
 
261
165
  def transform_graphql_args(args)
@@ -294,7 +198,7 @@ module Desiru
294
198
  # Remove trailing '?' for optional fields
295
199
  clean_name = field_name.to_s.gsub('?', '')
296
200
  parts = clean_name.split('_')
297
- parts[0] + parts[1..-1].map(&:capitalize).join
201
+ parts[0] + parts[1..].map(&:capitalize).join
298
202
  end
299
203
  end
300
204
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+ require_relative 'enum_builder'
5
+ require_relative 'type_cache_warmer'
6
+
7
+ module Desiru
8
+ module GraphQL
9
+ # Handles GraphQL type generation and caching
10
+ module TypeBuilder
11
+ extend self
12
+
13
+ @type_cache = {}
14
+ @cache_mutex = Mutex.new
15
+
16
+ class << self
17
+ attr_accessor :type_cache, :cache_mutex
18
+ end
19
+
20
+ def clear_type_cache!
21
+ @cache_mutex.synchronize do
22
+ @type_cache.clear
23
+ end
24
+ end
25
+
26
+ def build_output_type(signature)
27
+ # Create a stable cache key based on signature structure
28
+ cache_key = generate_type_cache_key('Output', signature.output_fields)
29
+
30
+ # Check cache first
31
+ @cache_mutex.synchronize do
32
+ return @type_cache[cache_key] if @type_cache[cache_key]
33
+ end
34
+
35
+ output_field_defs = build_field_definitions(signature.output_fields)
36
+
37
+ output_type = Class.new(::GraphQL::Schema::Object) do
38
+ graphql_name "Output#{cache_key.hash.abs}"
39
+ description 'Generated output type'
40
+
41
+ output_field_defs.each do |field_name, field_def|
42
+ field field_name, field_def[:type],
43
+ null: field_def[:null],
44
+ description: field_def[:description]
45
+ end
46
+ end
47
+
48
+ # Store in cache
49
+ @cache_mutex.synchronize do
50
+ @type_cache[cache_key] = output_type
51
+ end
52
+
53
+ output_type
54
+ end
55
+
56
+ def graphql_type_for_field(field)
57
+ case field.type
58
+ when :int, :integer
59
+ ::GraphQL::Types::Int
60
+ when :float
61
+ ::GraphQL::Types::Float
62
+ when :bool, :boolean
63
+ ::GraphQL::Types::Boolean
64
+ when :list
65
+ # Handle list types
66
+ element_type = graphql_type_for_element(field.element_type)
67
+ [element_type]
68
+ when :literal
69
+ # Create enum type for literal values
70
+ EnumBuilder.create_enum_type(field, @type_cache, @cache_mutex)
71
+ else
72
+ ::GraphQL::Types::String
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def build_field_definitions(fields)
79
+ field_defs = {}
80
+ fields.each do |field_name, field|
81
+ field_defs[camelcase_field_name(field_name)] = {
82
+ type: graphql_type_for_field(field),
83
+ null: field.optional,
84
+ description: field.description
85
+ }
86
+ end
87
+ field_defs
88
+ end
89
+
90
+ def graphql_type_for_element(element_type)
91
+ case element_type
92
+ when Hash
93
+ # Handle typed arrays like List[Literal['yes', 'no']]
94
+ if element_type[:type] == :literal
95
+ EnumBuilder.create_enum_from_values(element_type[:values], @type_cache, @cache_mutex)
96
+ else
97
+ ::GraphQL::Types::String
98
+ end
99
+ else
100
+ # Simple types
101
+ element_to_graphql_type(element_type)
102
+ end
103
+ end
104
+
105
+ def element_to_graphql_type(element_type)
106
+ case element_type
107
+ when :int, :integer then ::GraphQL::Types::Int
108
+ when :float then ::GraphQL::Types::Float
109
+ when :bool, :boolean then ::GraphQL::Types::Boolean
110
+ else ::GraphQL::Types::String
111
+ end
112
+ end
113
+
114
+ def camelcase_field_name(field_name)
115
+ # Convert snake_case to camelCase for GraphQL conventions
116
+ # Remove trailing '?' for optional fields
117
+ clean_name = field_name.to_s.gsub('?', '')
118
+ parts = clean_name.split('_')
119
+ parts[0] + parts[1..].map(&:capitalize).join
120
+ end
121
+
122
+ def generate_type_cache_key(prefix, fields)
123
+ # Generate a stable cache key based on field structure
124
+ # Use a more efficient approach with less string concatenation
125
+ field_data = fields.map do |name, field|
126
+ components = [name.to_s, field.type.to_s]
127
+ components << (field.optional ? 'T' : 'F')
128
+ components << field.element_type.to_s if field.respond_to?(:element_type)
129
+ components
130
+ end.sort
131
+
132
+ # Use hash for more compact cache keys
133
+ content_hash = field_data.hash.abs
134
+ "#{prefix}:#{content_hash}"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module GraphQL
5
+ # Utility for warming the GraphQL type cache with common types
6
+ module TypeCacheWarmer
7
+ extend self
8
+
9
+ # Pre-generate commonly used types to improve cold-start performance
10
+ def warm_common_types!
11
+ warm_common_output_types!
12
+ warm_common_enums!
13
+ end
14
+
15
+ # Get statistics about the type cache
16
+ def cache_stats
17
+ cache = TypeBuilder.instance_variable_get(:@type_cache)
18
+ mutex = TypeBuilder.instance_variable_get(:@cache_mutex)
19
+
20
+ mutex.synchronize do
21
+ {
22
+ total_types: cache.size,
23
+ output_types: cache.keys.count { |k| k.start_with?('Output:') },
24
+ enum_types: cache.keys.count { |k| k.include?('Enum') },
25
+ cache_keys: cache.keys
26
+ }
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def warm_common_output_types!
33
+ common_field_sets.each do |fields|
34
+ signature = create_mock_signature(fields)
35
+ TypeBuilder.build_output_type(signature)
36
+ end
37
+ end
38
+
39
+ def common_field_sets
40
+ [
41
+ # Single field types
42
+ { id: create_field(:string, false) },
43
+ { result: create_field(:string, true) },
44
+ { output: create_field(:string, true) },
45
+ { message: create_field(:string, true) },
46
+ {
47
+ id: create_field(:string, false),
48
+ result: create_field(:string, true),
49
+ timestamp: create_field(:float, true)
50
+ },
51
+ {
52
+ output: create_field(:string, true),
53
+ confidence: create_field(:float, true),
54
+ reasoning: create_field(:string, true)
55
+ },
56
+ {
57
+ success: create_field(:bool, false),
58
+ message: create_field(:string, true),
59
+ data: create_field(:string, true)
60
+ }
61
+ ]
62
+ end
63
+
64
+ def create_field(type, optional)
65
+ Struct.new(:type, :optional, :description).new(type, optional, nil)
66
+ end
67
+
68
+ def create_mock_signature(fields)
69
+ Struct.new(:output_fields).new(fields)
70
+ end
71
+
72
+ def warm_common_enums!
73
+ # Common enum value sets
74
+ common_enum_values = [
75
+ %w[pending processing completed failed],
76
+ %w[low medium high critical],
77
+ %w[yes no maybe],
78
+ %w[active inactive suspended],
79
+ %w[draft published archived]
80
+ ]
81
+
82
+ cache = TypeBuilder.instance_variable_get(:@type_cache)
83
+ mutex = TypeBuilder.instance_variable_get(:@cache_mutex)
84
+
85
+ common_enum_values.each do |values|
86
+ EnumBuilder.create_enum_from_values(values, cache, mutex)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -9,7 +9,7 @@ module Desiru
9
9
 
10
10
  def perform(job_id, module_class_name, signature_str, inputs, options = {})
11
11
  update_status(job_id, 'running', message: 'Initializing module')
12
-
12
+
13
13
  module_class = Object.const_get(module_class_name)
14
14
 
15
15
  # Extract module initialization parameters
@@ -18,7 +18,11 @@ module Desiru
18
18
  protected
19
19
 
20
20
  def store_result(job_id, result, ttl: 3600)
21
+ # Store in Redis for fast access
21
22
  redis.setex(result_key(job_id), ttl, result.to_json)
23
+
24
+ # Also persist to database for long-term storage
25
+ persist_result_to_db(job_id, result)
22
26
  end
23
27
 
24
28
  def fetch_result(job_id)
@@ -43,11 +47,74 @@ module Desiru
43
47
  status_data[:message] = message if message
44
48
 
45
49
  redis.setex(status_key(job_id), 86_400, status_data.to_json)
50
+
51
+ # Also persist to database
52
+ persist_status_to_db(job_id, status, progress: progress, message: message)
46
53
  end
47
54
 
48
55
  def status_key(job_id)
49
56
  "desiru:status:#{job_id}"
50
57
  end
58
+
59
+ # Database persistence methods
60
+ def persist_result_to_db(job_id, result)
61
+ return unless persistence_enabled?
62
+
63
+ job_repo.mark_completed(job_id, result)
64
+ rescue StandardError => e
65
+ Desiru.logger.warn("Failed to persist job result to database: #{e.message}")
66
+ end
67
+
68
+ def persist_error_to_db(job_id, error, backtrace = nil)
69
+ return unless persistence_enabled?
70
+
71
+ job_repo.mark_failed(job_id, error, backtrace: backtrace)
72
+ rescue StandardError => e
73
+ Desiru.logger.warn("Failed to persist job error to database: #{e.message}")
74
+ end
75
+
76
+ def persist_status_to_db(job_id, status, progress: nil, message: nil)
77
+ return unless persistence_enabled?
78
+
79
+ case status
80
+ when 'processing'
81
+ job_repo.mark_processing(job_id)
82
+ # Also update progress if provided
83
+ job_repo.update_progress(job_id, progress, message: message) if progress
84
+ when 'completed'
85
+ # Already handled by persist_result_to_db
86
+ when 'failed'
87
+ # Already handled by persist_error_to_db
88
+ else
89
+ job_repo.update_progress(job_id, progress, message: message) if progress
90
+ end
91
+ rescue StandardError => e
92
+ Desiru.logger.warn("Failed to persist job status to database: #{e.message}")
93
+ end
94
+
95
+ def create_job_record(job_id, inputs: nil, expires_at: nil)
96
+ return unless persistence_enabled?
97
+
98
+ job_repo.create_for_job(
99
+ job_id,
100
+ self.class.name,
101
+ self.class.get_sidekiq_options['queue'] || 'default',
102
+ inputs: inputs,
103
+ expires_at: expires_at
104
+ )
105
+ rescue StandardError => e
106
+ Desiru.logger.warn("Failed to create job record in database: #{e.message}")
107
+ end
108
+
109
+ def persistence_enabled?
110
+ Desiru::Persistence.enabled?
111
+ rescue StandardError
112
+ false
113
+ end
114
+
115
+ def job_repo
116
+ @job_repo ||= Desiru::Persistence.repositories[:job_results]
117
+ end
51
118
  end
52
119
  end
53
120
  end
@@ -10,7 +10,7 @@ module Desiru
10
10
  def perform(batch_id, module_class_name, signature_str, inputs_array, options = {})
11
11
  total_items = inputs_array.size
12
12
  update_status(batch_id, 'running', progress: 0, message: "Processing #{total_items} items")
13
-
13
+
14
14
  module_class = Object.const_get(module_class_name)
15
15
 
16
16
  # Extract module initialization parameters
@@ -34,9 +34,9 @@ module Desiru
34
34
 
35
35
  inputs_array.each_with_index do |inputs, index|
36
36
  progress = ((index + 1).to_f / total_items * 100).round
37
- update_status(batch_id, 'running', progress: progress,
38
- message: "Processing item #{index + 1} of #{total_items}")
39
-
37
+ update_status(batch_id, 'running', progress: progress,
38
+ message: "Processing item #{index + 1} of #{total_items}")
39
+
40
40
  result = module_instance.call(**inputs)
41
41
  results << {
42
42
  index: index,
@@ -53,8 +53,8 @@ module Desiru
53
53
  end
54
54
 
55
55
  final_status = errors.empty? ? 'completed' : 'completed_with_errors'
56
- update_status(batch_id, final_status, progress: 100,
57
- message: "Processed #{results.size} successfully, #{errors.size} failed")
56
+ update_status(batch_id, final_status, progress: 100,
57
+ message: "Processed #{results.size} successfully, #{errors.size} failed")
58
58
 
59
59
  store_result(batch_id, {
60
60
  success: errors.empty?,