Skip to content
This repository has been archived by the owner on Jul 28, 2020. It is now read-only.

Commit

Permalink
Add execution context for tasks and workflows (#79)
Browse files Browse the repository at this point in the history
* Add execution context for tasks and workflows

* Rename attempt_index to retry_index

* Make sure json is required
  • Loading branch information
idabmat authored Aug 27, 2019
1 parent aa65fc2 commit 43ee2e5
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added
- (De)Serialization support for instances of Class.
- Execution context for tasks and workflows
- Optional `on_error_retry_delay` method handling task failures and specifying
how many seconds to wait before retrying.

### Fixed
- Backport of ActiveSupport's `next_occurring` for older versions.
Expand Down
7 changes: 7 additions & 0 deletions lib/zenaton/contexts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Zenaton
# Represents the current runtime context of a Workflow or Task.
module Contexts
end
end
33 changes: 33 additions & 0 deletions lib/zenaton/contexts/task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Zenaton
module Contexts
# Represents the current runtime context of a Task.
#
# The information provided by the context can be useful to alter the
# behaviour of the task.
#
# For example, you can use the attempt index to know if a task has been
# automatically retried or not and how many times, and decide to do
# something when you did not expect the task to be retried more than X
# times.
#
# You can also use the attempt number in the `on_error_retry_delay` method
# of a task in order to implement complex retry strategies.
class Task
# @return [String] The UUID identifying the current task
attr_reader :id

# @return [Integer] The number of times this task has been automatically
# retried. This counter is reset if you issue a manual retry from your
# dashboard
attr_reader :retry_index

# @return [Zenaton::Contexts::Task] a new execution context for a task
def initialize(**kwargs)
@id = kwargs[:id]
@retry_index = kwargs[:retry_index]
end
end
end
end
17 changes: 17 additions & 0 deletions lib/zenaton/contexts/workflow.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Zenaton
module Contexts
# Represents the current runtime context of a Workflow.
class Workflow
# @return [String] The UUID identifying the current workflow
attr_reader :id

# @return [Zenaton::Contexts::Workflow] a new execution context for a
# workflow
def initialize(**kwargs)
@id = kwargs[:id]
end
end
end
end
31 changes: 30 additions & 1 deletion lib/zenaton/interfaces/task.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'zenaton/interfaces/job'
require 'zenaton/contexts/task'

module Zenaton
module Interfaces
Expand All @@ -9,7 +10,35 @@ class Task < Job
# Child classes should implement the handle method
def handle
raise NotImplemented,
"Your workflow does not implement the `handle' method"
"Your workflow does not implement the `#handle' method"
end

# (Optional) Implement this method for automatic retrial of task in
# case of failures.
# @param _exception [Exception] the reason for the task failure.
# @return [Integer, FalseClass, NilClass] the non-negative amount of
# seconds to wait before automatically retrying this task. Falsy values
# will avoid retrial. Other values will cause the retrial to fail.
def on_error_retry_delay(_exception)
nil
end

# @return [Zenaton::Contexts::Task] the task execution context
def context
@context || Contexts::Task.new
end

# @private
# Sets a new context if none has been set yet.
# This is called from the zenaton agent and will raise if called twice.
# @raise [ArgumentError] when the context was already set.
def add_context(**attributes)
message = <<~ERROR
Context has already been set and cannot be mutated.
ERROR
raise ArgumentError, message if @context

@context = Contexts::Task.new(attributes)
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/zenaton/interfaces/workflow.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'zenaton/interfaces/job'
require 'zenaton/contexts/workflow'

module Zenaton
module Interfaces
Expand All @@ -18,6 +19,20 @@ def handle
def id
nil
end

# @return [Zenaton::Contexts::Workflow] the workflow execution context
def context
@context || Contexts::Workflow.new
end

# @private
# Sets a new context if none has been set yet.
# This is called from the zenaton agent and will raise if called twice.
# @raise [ArgumentError] when the context was already set.
def add_context(**attributes)
raise ArgumentError if @context
@context = Contexts::Workflow.new(attributes)
end
end
end
end
1 change: 1 addition & 0 deletions lib/zenaton/services/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'json'
require 'net/http'
require 'zenaton/exceptions'
require 'json'

module Zenaton
# Collection of utility classes for the Zenaton library
Expand Down
1 change: 1 addition & 0 deletions lib/zenaton/services/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'json'
require 'zenaton/services/properties'
require 'json'

module Zenaton
module Services
Expand Down
53 changes: 53 additions & 0 deletions spec/zenaton/contexts/task_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require 'zenaton/contexts/task'

RSpec.describe Zenaton::Contexts::Task do
describe 'initialization' do
subject(:task_context) { described_class.new(**args) }

context 'when nothing is passed' do
let(:args) { {} }

it 'has no id' do
expect(task_context.id).to be_nil
end

it 'has no attempt index' do
expect(task_context.retry_index).to be_nil
end
end

context 'when id and attempt index are provided' do
let(:args) { { id: 'some-uuid', retry_index: 10 } }

it 'sets the id' do
expect(task_context.id).to eq('some-uuid')
end

it 'sets the attempt index' do
expect(task_context.retry_index).to eq(10)
end
end

context 'when other arguments are passed' do
let(:args) { { some: 'invalid', extra: 'attributes' } }

it 'has no id' do
expect(task_context.id).to be_nil
end

it 'has no attempt index' do
expect(task_context.retry_index).to be_nil
end

it 'does not set getter methods' do
expect { task_context.some }.to raise_error NoMethodError
end

it 'does not set the extra attributes' do
expect(task_context.instance_variables).to eq(%i[@id @retry_index])
end
end
end
end
41 changes: 41 additions & 0 deletions spec/zenaton/contexts/workflow_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'zenaton/contexts/workflow'

RSpec.describe Zenaton::Contexts::Workflow do
describe 'initialization' do
subject(:flow_context) { described_class.new(**args) }

context 'when nothing is passed' do
let(:args) { {} }

it 'has no id' do
expect(flow_context.id).to be_nil
end
end

context 'when id is provided' do
let(:args) { { id: 'some-uuid' } }

it 'sets the id' do
expect(flow_context.id).to eq('some-uuid')
end
end

context 'when other arguments are passed' do
let(:args) { { some: 'invalid', extra: 'attributes' } }

it 'has no id' do
expect(flow_context.id).to be_nil
end

it 'does not set getter methods' do
expect { flow_context.some }.to raise_error NoMethodError
end

it 'does not set the extra attributes' do
expect(flow_context.instance_variables).to eq([:@id])
end
end
end
end
67 changes: 66 additions & 1 deletion spec/zenaton/interfaces/task_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,76 @@
require 'zenaton/interfaces/task'

RSpec.describe Zenaton::Interfaces::Task do
let(:task) { described_class.new }
subject(:task) { described_class.new }

describe '#handle' do
it 'raises a not implemented error' do
expect { task.handle }.to raise_error Zenaton::NotImplemented
end
end

describe 'context' do
subject(:task_context) { task.context }

context 'when not yet set' do
it { is_expected.to be_a(Zenaton::Contexts::Task) }

it 'has no id' do
expect(task_context.id).to be_nil
end

it 'has no attempt index' do
expect(task_context.retry_index).to be_nil
end

it 'can be set' do
expect { task.add_context(id: 'some-uuid') }.not_to raise_error
end

it 'can update the context id' do
task.add_context(id: 'some-uuid')
expect(task_context.id).to eq('some-uuid')
end

it 'can update the context attempt index' do
task.add_context(retry_index: 10)
expect(task_context.retry_index).to eq(10)
end
end

context 'when already set' do
def safely_set(task, **attrs)
task.add_context(attrs)
rescue ArgumentError => e
e
end

before { task.add_context(id: 'some-uuid', retry_index: 10) }

it { is_expected.to be_a(Zenaton::Contexts::Task) }

it 'has an id' do
expect(task_context.id).to eq('some-uuid')
end

it 'has an attempt index' do
expect(task_context.retry_index).to eq(10)
end

it 'cannot be set' do
expect { task.add_context(id: 'some-uuid') }.to \
raise_error ArgumentError
end

it 'cannot update the context id' do
expect { safely_set(task, id: 'other-uuid') }.not_to \
change(task, :context)
end

it 'cannot update the context attempt index' do
expect { safely_set(task, retry_index: -1) }.not_to \
change(task, :context)
end
end
end
end
47 changes: 47 additions & 0 deletions spec/zenaton/interfaces/workflow_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,51 @@
expect { flow.handle }.to raise_error Zenaton::NotImplemented
end
end

describe 'context' do
subject(:flow_context) { flow.context }

context 'when not yet set' do
it { is_expected.to be_a(Zenaton::Contexts::Workflow) }

it 'has no id' do
expect(flow_context.id).to be_nil
end

it 'can be set' do
expect { flow.add_context(id: 'some-uuid') }.not_to raise_error
end

it 'can update the context id' do
flow.add_context(id: 'some-uuid')
expect(flow_context.id).to eq('some-uuid')
end
end

context 'when already set' do
def safely_set(flow, **attrs)
flow.add_context(attrs)
rescue ArgumentError => e
e
end

before { flow.add_context(id: 'some-uuid') }

it { is_expected.to be_a(Zenaton::Contexts::Workflow) }

it 'has an id' do
expect(flow_context.id).to eq('some-uuid')
end

it 'cannot be set' do
expect { flow.add_context(id: 'some-uuid') }.to \
raise_error ArgumentError
end

it 'cannot update the context id' do
expect { safely_set(flow, id: 'other-uuid') }.not_to \
change(flow, :context)
end
end
end
end

0 comments on commit 43ee2e5

Please sign in to comment.