chrono_forge 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be157f4c6a7dbf68348d63c5a67b3c07de4cb981dac1ef8e28e1eebe1d382088
4
- data.tar.gz: 4b60dcf5976495716fd1ff661a87dcc0b866daa8c48672723d2d438b5ced4f1d
3
+ metadata.gz: 1f2655b7f5ea191bd4c80e36b5e6e55439ad04f8570fb29d36ce0f6dc66ce687
4
+ data.tar.gz: ce5509c4fc6ee82179c6f14be7bf887f41f09ec0f344ef976ba3ac4691305725
5
5
  SHA512:
6
- metadata.gz: 473a8bb888d88fb8df928c935a8fdeb9941fdfa8cd006e7e90491ce02c64fd73bc5e185c45db94dd8d4152385f8ffcae594ec5a82b7bbc6b7aecfddbf9d147bc
7
- data.tar.gz: 0ba90ad3610d55c0dd7c092b6d7140de86a7a380411685d777d9a4aa6c172836930cef5aa0ffbeeda6dbbab34a0a0faff4a4928dc50e5e037e20038fff12863a
6
+ metadata.gz: baca6968fedbded14682da7a4fb68b3d2f5f3da4228671358461c77245920787d0672fd4886ad000693a06bb8e22438d4f311b66dabfdbbd917dfad5d003cea2
7
+ data.tar.gz: 677a8ef2549511e7e03f18c45d34897ac0ceaaa25ea9a11300ecac711f552c92c074e56ea6453d21a1b58d0ba20aabdd859941bbb1e93eec8cf3b7e9a6c69b34
data/Appraisals CHANGED
@@ -1,4 +1,3 @@
1
1
  appraise "rails-7.1" do
2
2
  gem "rails", "~> 7.1", ">= 7.1.3.4"
3
- gem "sqlite3", "~> 1.4"
4
3
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 7.1", ">= 7.1.3.4"
6
5
  gem "sqlite3", "~> 1.4"
6
+ gem "rails", "~> 7.1", ">= 7.1.3.4"
7
7
 
8
8
  gemspec path: "../"
@@ -2,7 +2,8 @@ PATH
2
2
  remote: ..
3
3
  specs:
4
4
  chrono_forge (0.0.1)
5
- rails
5
+ activejob
6
+ activerecord
6
7
  zeitwerk
7
8
 
8
9
  GEM
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: chrono_forge_error_logs
6
+ #
7
+ # id :integer not null, primary key
8
+ # backtrace :text
9
+ # context :json
10
+ # error_class :string
11
+ # error_message :text
12
+ # created_at :datetime not null
13
+ # updated_at :datetime not null
14
+ # workflow_id :integer not null
15
+ #
16
+ # Indexes
17
+ #
18
+ # index_chrono_forge_error_logs_on_workflow_id (workflow_id)
19
+ #
20
+ # Foreign Keys
21
+ #
22
+ # workflow_id (workflow_id => chrono_forge_workflows.id)
23
+ #
24
+
25
+ module ChronoForge
26
+ class ErrorLog < ActiveRecord::Base
27
+ self.table_name = "chrono_forge_error_logs"
28
+
29
+ belongs_to :workflow
30
+
31
+ # Cleanup method
32
+ def self.cleanup_old_logs(retention_period: 30.days)
33
+ where("created_at < ?", retention_period.ago).delete_all
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: chrono_forge_execution_logs
6
+ #
7
+ # id :integer not null, primary key
8
+ # attempts :integer default(0), not null
9
+ # completed_at :datetime
10
+ # error_class :string
11
+ # error_message :text
12
+ # last_executed_at :datetime
13
+ # metadata :json
14
+ # started_at :datetime
15
+ # state :integer default("pending"), not null
16
+ # step_name :string not null
17
+ # created_at :datetime not null
18
+ # updated_at :datetime not null
19
+ # workflow_id :integer not null
20
+ #
21
+ # Indexes
22
+ #
23
+ # idx_on_workflow_id_step_name_11bea8586e (workflow_id,step_name) UNIQUE
24
+ # index_chrono_forge_execution_logs_on_workflow_id (workflow_id)
25
+ #
26
+ # Foreign Keys
27
+ #
28
+ # workflow_id (workflow_id => chrono_forge_workflows.id)
29
+ #
30
+ module ChronoForge
31
+ class ExecutionLog < ActiveRecord::Base
32
+ self.table_name = "chrono_forge_execution_logs"
33
+
34
+ belongs_to :workflow
35
+
36
+ enum :state, %i[
37
+ pending
38
+ completed
39
+ failed
40
+ ]
41
+
42
+ # Cleanup method
43
+ def self.cleanup_old_logs(retention_period: 30.days)
44
+ where("created_at < ?", retention_period.ago).delete_all
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,68 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class Context
4
+ class ValidationError < Error; end
5
+
6
+ ALLOWED_TYPES = [
7
+ String,
8
+ Integer,
9
+ Float,
10
+ TrueClass,
11
+ FalseClass,
12
+ NilClass,
13
+ Hash,
14
+ Array
15
+ ]
16
+
17
+ def initialize(workflow)
18
+ @workflow = workflow
19
+ @context = workflow.context || {}
20
+ @dirty = false
21
+ end
22
+
23
+ def []=(key, value)
24
+ # Type and size validation
25
+ validate_value!(value)
26
+
27
+ @context[key.to_s] =
28
+ if value.is_a?(Hash) || value.is_a?(Array)
29
+ deep_dup(value)
30
+ else
31
+ value
32
+ end
33
+
34
+ @dirty = true
35
+ end
36
+
37
+ def [](key)
38
+ @context[key.to_s]
39
+ end
40
+
41
+ def save!
42
+ return unless @dirty
43
+
44
+ @workflow.update_column(:context, @context)
45
+ @dirty = false
46
+ end
47
+
48
+ private
49
+
50
+ def validate_value!(value)
51
+ unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }
52
+ raise ValidationError, "Unsupported context value type: #{value.inspect}"
53
+ end
54
+
55
+ # Optional: Add size constraints
56
+ if value.is_a?(String) && value.size > 64.kilobytes
57
+ raise ValidationError, "Context value too large"
58
+ end
59
+ end
60
+
61
+ def deep_dup(obj)
62
+ JSON.parse(JSON.generate(obj))
63
+ rescue
64
+ obj.dup
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class ExecutionTracker
4
+ def self.track_error(workflow, error)
5
+ # Create a detailed error log
6
+ ErrorLog.create!(
7
+ workflow: workflow,
8
+ error_class: error.class.name,
9
+ error_message: error.message,
10
+ backtrace: error.backtrace.join("\n"),
11
+ context: workflow.context
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class LongRunningConcurrentExecutionError < Error; end
4
+
5
+ class ConcurrentExecutionError < Error; end
6
+
7
+ class LockStrategy
8
+ def self.acquire_lock(job_id, workflow, max_duration:)
9
+ ActiveRecord::Base.transaction do
10
+ # Find the workflow with a lock, considering stale locks
11
+ workflow = workflow.lock!
12
+
13
+ # Check for active execution
14
+ if workflow.locked_at && workflow.locked_at > max_duration.ago
15
+ raise ConcurrentExecutionError, "Job currently in progress"
16
+ end
17
+
18
+ # Atomic update of lock status
19
+ workflow.update_columns(
20
+ locked_by: job_id,
21
+ locked_at: Time.current,
22
+ state: :running
23
+ )
24
+
25
+ workflow
26
+ end
27
+ end
28
+
29
+ def self.release_lock(job_id, workflow)
30
+ workflow = workflow.reload
31
+ if workflow.locked_by != job_id
32
+ raise LongRunningConcurrentExecutionError,
33
+ "#{self.class}(#{job_id}) executed longer than specified max_duration, " \
34
+ "allowing another instance(#{workflow.locked_by}) to acquire the lock."
35
+ end
36
+
37
+ columns = {locked_at: nil, locked_by: nil}
38
+ columns[:state] = :idle if workflow.running?
39
+
40
+ workflow.update_columns(columns)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,75 @@
1
+ module ChronoForge
2
+ module Executor
3
+ module Methods
4
+ module DurablyExecute
5
+ def durably_execute(method, **options)
6
+ # Create execution log
7
+ step_name = "durably_execute$#{method}"
8
+ execution_log = ExecutionLog.create_or_find_by!(
9
+ workflow: @workflow,
10
+ step_name: step_name
11
+ ) do |log|
12
+ log.started_at = Time.current
13
+ end
14
+
15
+ # Return if already completed
16
+ return execution_log.metadata["result"] if execution_log.completed?
17
+
18
+ # Execute with error handling
19
+ begin
20
+ # Update execution log with attempt
21
+ execution_log.update!(
22
+ attempts: execution_log.attempts + 1,
23
+ last_executed_at: Time.current
24
+ )
25
+
26
+ # Execute the method
27
+ result = if method.is_a?(Symbol)
28
+ send(method)
29
+ else
30
+ method.call(@context)
31
+ end
32
+
33
+ # Complete the execution
34
+ execution_log.update!(
35
+ state: :completed,
36
+ completed_at: Time.current,
37
+ metadata: {result: result}
38
+ )
39
+ result
40
+ rescue HaltExecutionFlow
41
+ raise
42
+ rescue => e
43
+ # Log the error
44
+ Rails.logger.error { "Error while durably executing #{method}: #{e.message}" }
45
+ self.class::ExecutionTracker.track_error(workflow, e)
46
+
47
+ # Optional retry logic
48
+ if execution_log.attempts < (options[:max_attempts] || 3)
49
+ # Reschedule with exponential backoff
50
+ backoff = (2**[execution_log.attempts || 1, 5].min).seconds
51
+
52
+ self.class
53
+ .set(wait: backoff)
54
+ .perform_later(
55
+ @workflow.key,
56
+ retry_method: method
57
+ )
58
+
59
+ # Halt current execution
60
+ halt_execution!
61
+ else
62
+ # Max attempts reached
63
+ execution_log.update!(
64
+ state: :failed,
65
+ error_message: e.message,
66
+ error_class: e.class.name
67
+ )
68
+ raise ExecutionFailedError, "#{step_name} failed after maximum attempts"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ module ChronoForge
2
+ module Executor
3
+ module Methods
4
+ module Wait
5
+ def wait(duration, name, **options)
6
+ # Create execution log
7
+ execution_log = ExecutionLog.create_or_find_by!(
8
+ workflow: @workflow,
9
+ step_name: "wait$#{name}"
10
+ ) do |log|
11
+ log.started_at = Time.current
12
+ log.metadata = {
13
+ wait_until: duration.from_now
14
+ }
15
+ end
16
+
17
+ # Return if already completed
18
+ return if execution_log.completed?
19
+
20
+ # Check if wait period has passed
21
+ if Time.current >= Time.parse(execution_log.metadata["wait_until"])
22
+ execution_log.update!(
23
+ attempts: execution_log.attempts + 1,
24
+ state: :completed,
25
+ completed_at: Time.current,
26
+ last_executed_at: Time.current
27
+ )
28
+ return
29
+ end
30
+
31
+ execution_log.update!(
32
+ attempts: execution_log.attempts + 1,
33
+ last_executed_at: Time.current
34
+ )
35
+
36
+ # Reschedule the job
37
+ self.class
38
+ .set(wait: duration)
39
+ .perform_later(@workflow.key)
40
+
41
+ # Halt current execution
42
+ halt_execution!
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,110 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class WaitConditionNotMet < ExecutionFailedError; end
4
+
5
+ module Methods
6
+ module WaitUntil
7
+ def wait_until(condition, **options)
8
+ # Default timeout and check interval
9
+ timeout = options[:timeout] || 10.seconds
10
+ check_interval = options[:check_interval] || 1.second
11
+
12
+ # Find or create execution log
13
+ step_name = "wait_until$#{condition}"
14
+ execution_log = ExecutionLog.create_or_find_by!(
15
+ workflow: @workflow,
16
+ step_name: step_name
17
+ ) do |log|
18
+ log.started_at = Time.current
19
+ log.metadata = {
20
+ timeout_at: timeout.from_now,
21
+ check_interval: check_interval,
22
+ condition: condition.to_s
23
+ }
24
+ end
25
+
26
+ # Return if already completed
27
+ if execution_log.completed?
28
+ return execution_log.metadata["result"]
29
+ end
30
+
31
+ # Evaluate condition
32
+ begin
33
+ execution_log.update!(
34
+ attempts: execution_log.attempts + 1,
35
+ last_executed_at: Time.current
36
+ )
37
+
38
+ condition_met = if condition.is_a?(Proc)
39
+ condition.call(@context)
40
+ elsif condition.is_a?(Symbol)
41
+ send(condition)
42
+ else
43
+ raise ArgumentError, "Unsupported condition type"
44
+ end
45
+ rescue HaltExecutionFlow
46
+ raise
47
+ rescue => e
48
+ # Log the error
49
+ Rails.logger.error { "Error evaluating condition #{condition}: #{e.message}" }
50
+ self.class::ExecutionTracker.track_error(workflow, e)
51
+
52
+ # Optional retry logic
53
+ if (options[:retry_on] || []).include?(e.class)
54
+ # Reschedule with exponential backoff
55
+ backoff = (2**[execution_log.attempts || 1, 5].min).seconds
56
+
57
+ self.class
58
+ .set(wait: backoff)
59
+ .perform_later(
60
+ @workflow.key
61
+ )
62
+
63
+ # Halt current execution
64
+ halt_execution!
65
+ else
66
+ execution_log.update!(
67
+ state: :failed,
68
+ error_message: e.message,
69
+ error_class: e.class.name
70
+ )
71
+ raise ExecutionFailedError, "#{step_name} failed with an error: #{e.message}"
72
+ end
73
+ end
74
+
75
+ # Handle condition met
76
+ if condition_met
77
+ execution_log.update!(
78
+ state: :completed,
79
+ completed_at: Time.current,
80
+ metadata: execution_log.metadata.merge("result" => true)
81
+ )
82
+ return true
83
+ end
84
+
85
+ # Check for timeout
86
+ metadata = execution_log.metadata
87
+ if Time.current > metadata["timeout_at"]
88
+ execution_log.update!(
89
+ state: :failed,
90
+ metadata: metadata.merge("result" => nil)
91
+ )
92
+ Rails.logger.warn { "Timeout reached for condition #{condition}. Condition not met within the timeout period." }
93
+ raise WaitConditionNotMet, "Condition not met within timeout period"
94
+ end
95
+
96
+ # Reschedule with delay
97
+ self.class
98
+ .set(wait: check_interval)
99
+ .perform_later(
100
+ @workflow.key,
101
+ wait_condition: condition
102
+ )
103
+
104
+ # Halt current execution
105
+ halt_execution!
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,9 @@
1
+ module ChronoForge
2
+ module Executor
3
+ module Methods
4
+ include Methods::Wait
5
+ include Methods::WaitUntil
6
+ include Methods::DurablyExecute
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class RetryStrategy
4
+ BACKOFF_STRATEGY = [
5
+ 1.second, # Initial retry
6
+ 5.seconds, # Second retry
7
+ 30.seconds, # Third retry
8
+ 2.minutes, # Fourth retry
9
+ 10.minutes # Final retry
10
+ ]
11
+
12
+ def self.schedule_retry(workflow, attempt: 0)
13
+ wait_duration = BACKOFF_STRATEGY[attempt] || BACKOFF_STRATEGY.last
14
+
15
+ # Schedule with exponential backoff
16
+ workflow.job_klass.constantize
17
+ .set(wait: wait_duration)
18
+ .perform_later(
19
+ workflow.key,
20
+ attempt: attempt + 1
21
+ )
22
+ end
23
+
24
+ def self.max_attempts
25
+ BACKOFF_STRATEGY.length
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,140 @@
1
+ module ChronoForge
2
+ module Executor
3
+ class Error < StandardError; end
4
+
5
+ class ExecutionFailedError < Error; end
6
+
7
+ class ExecutionFlowControl < Error; end
8
+
9
+ class HaltExecutionFlow < ExecutionFlowControl; end
10
+
11
+ include Methods
12
+
13
+ def perform(key, attempt: 0, **kwargs)
14
+ # Prevent excessive retries
15
+ if attempt >= self.class::RetryStrategy.max_attempts
16
+ Rails.logger.error { "Max attempts reached for job #{key}" }
17
+ return
18
+ end
19
+
20
+ # Find or create job with comprehensive tracking
21
+ setup_workflow(key, kwargs)
22
+
23
+ begin
24
+ # Skip if workflow cannot be executed
25
+ return unless workflow.executable?
26
+
27
+ # Acquire lock with advanced concurrency protection
28
+ self.class::LockStrategy.acquire_lock(job_id, workflow, max_duration: max_duration)
29
+
30
+ # Execute core job logic
31
+ super(**workflow.kwargs.symbolize_keys)
32
+
33
+ # Mark as complete
34
+ complete_workflow!
35
+ rescue ExecutionFailedError => e
36
+ Rails.logger.error { "Execution step failed for #{key}" }
37
+ self.class::ExecutionTracker.track_error(workflow, e)
38
+ workflow.stalled!
39
+ nil
40
+ rescue HaltExecutionFlow
41
+ # Halt execution
42
+ Rails.logger.debug { "Execution halted for #{key}" }
43
+ nil
44
+ rescue ConcurrentExecutionError
45
+ # Graceful handling of concurrent execution
46
+ Rails.logger.warn { "Concurrent execution detected for job #{key}" }
47
+ nil
48
+ rescue => e
49
+ Rails.logger.error { "An error occurred during execution of #{key}" }
50
+ self.class::ExecutionTracker.track_error(workflow, e)
51
+
52
+ # Retry if applicable
53
+ if should_retry?(e, attempt)
54
+ self.class::RetryStrategy.schedule_retry(workflow, attempt: attempt)
55
+ else
56
+ workflow.failed!
57
+ end
58
+ ensure
59
+ context.save!
60
+ # Always release the lock
61
+ self.class::LockStrategy.release_lock(job_id, workflow)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def complete_workflow!
68
+ # Create an execution log for workflow completion
69
+ execution_log = ExecutionLog.create_or_find_by!(
70
+ workflow: workflow,
71
+ step_name: "$workflow_completion$"
72
+ ) do |log|
73
+ log.started_at = Time.current
74
+ log.metadata = {
75
+ workflow_id: workflow.id
76
+ }
77
+ end
78
+
79
+ begin
80
+ execution_log.update!(
81
+ attempts: execution_log.attempts + 1,
82
+ last_executed_at: Time.current
83
+ )
84
+
85
+ workflow.completed_at = Time.current
86
+ workflow.completed!
87
+
88
+ # Mark execution log as completed
89
+ execution_log.update!(
90
+ state: :completed,
91
+ completed_at: Time.current
92
+ )
93
+
94
+ # Return the execution log for tracking
95
+ execution_log
96
+ rescue => e
97
+ # Log any completion errors
98
+ execution_log.update!(
99
+ state: :failed,
100
+ error_message: e.message,
101
+ error_class: e.class.name
102
+ )
103
+ raise
104
+ end
105
+ end
106
+
107
+ def setup_workflow(key, kwargs)
108
+ @workflow = find_workflow(key, kwargs)
109
+ @context = Context.new(@workflow)
110
+ end
111
+
112
+ def find_workflow(key, kwargs)
113
+ Workflow.create_or_find_by!(key: key) do |workflow|
114
+ workflow.job_klass = self.class.to_s
115
+ workflow.kwargs = kwargs
116
+ workflow.started_at = Time.current
117
+ end
118
+ end
119
+
120
+ def should_retry?(error, attempt_count)
121
+ attempt_count < 3
122
+ end
123
+
124
+ def halt_execution!
125
+ raise HaltExecutionFlow
126
+ end
127
+
128
+ def workflow
129
+ @workflow
130
+ end
131
+
132
+ def context
133
+ @context
134
+ end
135
+
136
+ def max_duration
137
+ 10.minutes
138
+ end
139
+ end
140
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: chrono_forge_workflows
6
+ #
7
+ # id :integer not null, primary key
8
+ # completed_at :datetime
9
+ # context :json not null
10
+ # job_klass :string not null
11
+ # key :string not null
12
+ # kwargs :json not null
13
+ # locked_at :datetime
14
+ # started_at :datetime
15
+ # state :integer default("idle"), not null
16
+ # created_at :datetime not null
17
+ # updated_at :datetime not null
18
+ #
19
+ # Indexes
20
+ #
21
+ # index_chrono_forge_workflows_on_key (key) UNIQUE
22
+ #
23
+ module ChronoForge
24
+ class Workflow < ActiveRecord::Base
25
+ self.table_name = "chrono_forge_workflows"
26
+
27
+ has_many :execution_logs
28
+ has_many :error_logs
29
+
30
+ enum :state, %i[
31
+ idle
32
+ running
33
+ completed
34
+ failed
35
+ stalled
36
+ ]
37
+
38
+ # Cleanup method
39
+ def self.cleanup_old_logs(retention_period: 30.days)
40
+ where("created_at < ?", retention_period.ago).delete_all
41
+ end
42
+
43
+ # Serialization for metadata
44
+ serialize :metadata, coder: JSON
45
+
46
+ def executable?
47
+ idle? || running?
48
+ end
49
+ end
50
+ end
data/lib/chrono_forge.rb CHANGED
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "chrono_forge/version"
4
-
5
3
  require "zeitwerk"
6
- require "active_support/core_ext/object/blank"
4
+ require "active_record"
5
+ require "active_job"
7
6
 
8
7
  module Chronoforge
9
- Loader = Zeitwerk::Loader.new.tap do |loader|
10
- loader.tag = File.basename(__FILE__, ".rb")
8
+ Loader = Zeitwerk::Loader.for_gem.tap do |loader|
11
9
  loader.ignore("#{__dir__}/generators")
12
10
  loader.setup
13
11
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Installs ChronoForge
3
+
4
+ Example:
5
+ bin/rails g chrono_form:install
6
+
7
+ This will create a new migration e.g:
8
+ 20241221181505_install_chrono_forge.rb
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record/migration"
4
+
5
+ module ChronoForge
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ::ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def start
12
+ install_migrations
13
+ rescue => err
14
+ say "#{err.class}: #{err}\n#{err.backtrace.join("\n")}", :red
15
+ exit 1
16
+ end
17
+
18
+ private
19
+
20
+ def install_migrations
21
+ migration_template "install_chrono_forge.rb", "install_chrono_forge.rb"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InstallChronoForge < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :chrono_forge_workflows do |t|
6
+ t.string :key, null: false, index: {unique: true}
7
+ t.string :job_klass, null: false
8
+
9
+ if t.respond_to?(:jsonb)
10
+ t.jsonb :kwargs, null: false, default: {}
11
+ t.jsonb :options, null: false, default: {}
12
+ t.jsonb :context, null: false, default: {}
13
+ else
14
+ t.json :kwargs, null: false, default: {}
15
+ t.json :options, null: false, default: {}
16
+ t.json :context, null: false, default: {}
17
+ end
18
+
19
+ t.integer :state, null: false, default: 0
20
+ t.string :locked_by
21
+ t.datetime :locked_at
22
+
23
+ t.datetime :started_at
24
+ t.datetime :completed_at
25
+
26
+ t.timestamps
27
+ end
28
+
29
+ create_table :chrono_forge_execution_logs do |t|
30
+ t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}
31
+ t.string :step_name, null: false
32
+ t.integer :attempts, null: false, default: 0
33
+ t.datetime :started_at
34
+ t.datetime :last_executed_at
35
+ t.datetime :completed_at
36
+ if t.respond_to?(:jsonb)
37
+ t.jsonb :metadata
38
+ else
39
+ t.json :metadata
40
+ end
41
+ t.integer :state, null: false, default: 0
42
+ t.string :error_class
43
+ t.text :error_message
44
+
45
+ t.timestamps
46
+ t.index %i[workflow_id step_name], unique: true
47
+ end
48
+
49
+ create_table :chrono_forge_error_logs do |t|
50
+ t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}
51
+ t.string :error_class
52
+ t.text :error_message
53
+ t.text :backtrace
54
+ if t.respond_to?(:jsonb)
55
+ t.jsonb :context
56
+ else
57
+ t.json :context
58
+ end
59
+
60
+ t.timestamps
61
+ end
62
+ end
63
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
@@ -11,7 +11,21 @@ cert_chain: []
11
11
  date: 2024-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activejob
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
@@ -169,7 +183,22 @@ files:
169
183
  - gemfiles/rails_7.1.gemfile
170
184
  - gemfiles/rails_7.1.gemfile.lock
171
185
  - lib/chrono_forge.rb
186
+ - lib/chrono_forge/error_log.rb
187
+ - lib/chrono_forge/execution_log.rb
188
+ - lib/chrono_forge/executor.rb
189
+ - lib/chrono_forge/executor/context.rb
190
+ - lib/chrono_forge/executor/execution_tracker.rb
191
+ - lib/chrono_forge/executor/lock_strategy.rb
192
+ - lib/chrono_forge/executor/methods.rb
193
+ - lib/chrono_forge/executor/methods/durably_execute.rb
194
+ - lib/chrono_forge/executor/methods/wait.rb
195
+ - lib/chrono_forge/executor/methods/wait_until.rb
196
+ - lib/chrono_forge/executor/retry_strategy.rb
172
197
  - lib/chrono_forge/version.rb
198
+ - lib/chrono_forge/workflow.rb
199
+ - lib/generators/chrono_forge/install/USAGE
200
+ - lib/generators/chrono_forge/install/install_generator.rb
201
+ - lib/generators/chrono_forge/install/templates/install_chrono_forge.rb
173
202
  - sig/chrono_forge.rbs
174
203
  homepage: https://github.com/radioactive-labs/chrono_forge
175
204
  licenses: