chrono_forge 0.0.1 → 0.0.2

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 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: