Class: Gross::Machine

Inherits:
Object
  • Object
show all
Defined in:
lib/gross/machine.rb,
lib/gross/tasks/if.rb,
lib/gross/tasks/set.rb,
lib/gross/tasks/case.rb,
lib/gross/tasks/print.rb,
lib/gross/tasks/rprint.rb,
lib/gross/tasks/blocker.rb,
lib/gross/tasks/conditional.rb

Overview

The machine that will run and backtrack the tasks.

Examples:

Simple Hello World machine

g = Gross::Machine.new
g.print 'Hello, World!'
g.run

Instance Attribute Summary collapse

Tasks::IfElse collapse

Tasks collapse

Tasks::Debug collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = 'MAIN', context = OpenStruct.new) ⇒ Machine

Initializes a machine with a given name

Parameters:

  • name (String) (defaults to: 'MAIN')

    The name of the machine, will be used in logs

  • context (OpenStruct) (defaults to: OpenStruct.new)

    The variables that should be available


36
37
38
39
40
41
# File 'lib/gross/machine.rb', line 36

def initialize(name = 'MAIN', context = OpenStruct.new)
    @tasks = []
    @context = context
    @queue = Queue.new
    @name = name
end

Instance Attribute Details

#nameString (readonly)

Returns the name of this machine

This name should only be used for logging purposes, it is not at all supposed to be unique

Returns:

  • (String)

    The name of this machine


50
51
52
# File 'lib/gross/machine.rb', line 50

def name
  @name
end

#queueQueue<Message> (readonly)

Returns the command queue to this machine

Any message can be sent to the current machine.

Returns:

  • (Queue<Message>)

    The command queue that can be used to send messages to the machine

See Also:


60
61
62
# File 'lib/gross/machine.rb', line 60

def queue
  @queue
end

Instance Method Details

#add_custom_task(block) ⇒ Task

Adds a task to be run

parameters, and returns the task to be added

Parameters:

  • block (#call)

    A function that takes a task ID, machine name and command queue as

Returns:

  • (Task)

    The task just added


70
71
72
73
74
75
# File 'lib/gross/machine.rb', line 70

def add_custom_task(block)
    new_task = block.call @tasks.length, self
    Gross::log.debug "Adding task[#{new_task.hrid}]: #{new_task.name}"
    @tasks << new_task
    return new_task
end

#add_task(name: '', up: lambda {}, down: lambda {}, instant: false) ⇒ Task

Adds a task to be run

Parameters:

  • name (String)

    The name of the task, will be used in logs

  • up (#call)

    The function to call when the task should go up

  • down (#call)

    The function to call when the task should go down

Returns:

  • (Task)

    The task just added


86
87
88
89
90
# File 'lib/gross/machine.rb', line 86

def add_task(name: '', up: lambda {}, down: lambda {}, instant: false)
    add_custom_task (lambda do |id, machine|
        return Task.new(id, name, machine, instant, up, down)
    end)
end

#blocker(name = 'blocker') ⇒ Task

Adds a blocker task

A blocker is a dummy task, whose only aim is to provide a backtrack point

Parameters:

  • name (String) (defaults to: 'blocker')

    A human-readable name for the task

Returns:

  • (Task)

    The created blocker task


60
61
62
63
64
# File 'lib/gross/tasks/blocker.rb', line 60

def blocker(name='blocker')
    add_custom_task (lambda do |id, machine|
        return BlockerTask.new(id, name, machine)
    end)
end

#case(value, dict, default = ->(h) {}, name = 'conditional') ⇒ Task

Adds a case task

A case task will evaluate a value, and then execute the code corresponding to the computed value, inside a dictionary – defaulting to a default execution.

Parameters:

  • value (ContextCallable)

    is the value, given as a ContextCallable

  • dict (Hash<Object, MachineCallable>)

    is the dictionary, with as keys the possible values. If value is a key in this array, then the correspondingg value will be evaluated as a MachineCallable

  • default (MachineCallable) (defaults to: ->(h) {})

    A machine callable to call in case no key matches

  • name (String) (defaults to: 'conditional')

    A human-readable name for the task

Returns:

  • (Task)

    A task that implements the case behaviour defined above


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/gross/tasks/case.rb', line 38

def case(value, dict, default = ->(h) {}, name='conditional')
    machine = nil
    queue = Queue.new
    thread = nil
    task = nil
    task = add_task(
        name: name,
        up:   lambda do
            machine = Machine.new "#{task.hrid}[#{name}]", @context
            v = value.call @context
            if dict.has_key? v
                dict[v].call machine
            else
                default.call machine
            end
            thread = Thread.new { machine.run queue }
            while queue.pop != :up; end
        end,
        down: lambda do
            machine.queue << Message.exit
            thread.join
        end
    )
end

#conditional(name = 'conditional', args) ⇒ Task

Adds a conditional task

A conditional task will evaluate a series of conditions, stopping at the first one which evaluates to true and executing the corresponding code

The argument args is defined as a list of (name, condition, code) triplets.

name

name is the human-readable name by which name the internally-generated machine — actual name will be current_task.hrid. Should be set as a name clearly identifying the condition

condition

condition is the condition given as a ContextCallable

code

code is the code generator, given as a MachineCallable

Parameters:

  • name (String) (defaults to: 'conditional')

    A human-readable name for the task

  • args (Array<Array<(String, #call, #call)>>)

    The elements to evaluate, see description above

Returns:

  • (Task)

    A task that implements the conditional behaviour defined above


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/gross/tasks/conditional.rb', line 41

def conditional(name='conditional', args)
    machine = nil
    queue = Queue.new
    thread = nil
    task = nil
    task = add_task(
        name: name,
        up:   lambda do
            args.each do |name, cond, code|
                if cond.call @context
                    machine = Machine.new "#{task.hrid}[#{name}]", @context
                    code.call machine
                    thread = Thread.new { machine.run queue }
                    while queue.pop != :up; end
                    break
                end
            end
        end,
        down: lambda do
            machine.queue << Message.exit
            thread.join
        end
    )
end

#else(name = 'else', &code) ⇒ void

This method returns an undefined value.

Prepares a conditional task by adding it a catch-all element

Parameters:

  • name (String) (defaults to: 'else')

    The human-readable name for the condition

  • code (#call)

    The code as a MachineCallable


55
56
57
# File 'lib/gross/tasks/if.rb', line 55

def else(name='else', &code)
    @current_if << [name, lambda { |c| true }, code]
end

#elsif(name = 'elsif', cond, &code) ⇒ void

This method returns an undefined value.

Prepares a conditional task by adding it some elements

Parameters:

  • name (String) (defaults to: 'elsif')

    The human-readable name for the condition

  • cond (#call)

    The condition as a ContextCallable

  • code (#call)

    The code as a MachineCallable


44
45
46
# File 'lib/gross/tasks/if.rb', line 44

def elsif(name='elsif', cond, &code)
    @current_if << [name, cond, code]
end

#end(name = 'conditional') ⇒ Task

Completes conditional task just built, returning the generated task

Parameters:

  • name (String) (defaults to: 'conditional')

    A name for the entire generated if/then/else conditional

Returns:


67
68
69
70
71
# File 'lib/gross/tasks/if.rb', line 67

def end(name='conditional')
    args = @current_if
    @current_if = nil
    return conditional(name, args)
end

#if(name = 'if', cond, &code) ⇒ void

This method returns an undefined value.

Prepares a conditional task with its first element

Parameters:

  • name (String) (defaults to: 'if')

    The human-readable name for the condition

  • cond (#call)

    The condition as a ContextCallable

  • code (#call)

    The code as a MachineCallable


34
35
36
# File 'lib/gross/tasks/if.rb', line 34

def if(name='if', cond, &code)
    @current_if = [[name, cond, code]]
end

Writes a message to stdout when task is brought up

Overloads:

  • #print(message) ⇒ Task

    Parameters:

  • #print(&block) ⇒ Task

    Parameters:

Returns:

  • (Task)

    A task that prints the message given as a parameter when being brought up


33
34
35
36
37
38
39
40
# File 'lib/gross/tasks/print.rb', line 33

def print(message='', &block)
    name, msg = context_callable(message, &block)
    add_task(
        name: "print '#{name}'",
        instant: true,
        up: lambda { $stdout.print(msg.call @context) }
    )
end

#rprint(message) ⇒ Task #rprint(&block) ⇒ Task

Writes a message to stdout when task is brought down

Overloads:

  • #rprint(message) ⇒ Task

    Parameters:

  • #rprint(&block) ⇒ Task

    Parameters:

Returns:

  • (Task)

    A task that prints the message given as a parameter when being brought down


33
34
35
36
37
38
39
40
# File 'lib/gross/tasks/rprint.rb', line 33

def rprint(message='', &block)
    name, msg = context_callable(message, &block)
    add_task(
        name: "rprint '#{name}'",
        instant: true,
        down: lambda { $stdout.print(msg.call @context) }
    )
end

#run(extqueue = nil) ⇒ void

This method returns an undefined value.

Runs the machine, backtracking when needed

Parameters:

  • extqueue (Queue<:up, :down, :down_done>) (defaults to: nil)

    The queue that will be used to output messages reporting when all the states are up and when a state is going down, requiring possible backtracking of machines that depend on this one


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/gross/machine.rb', line 100

def run(extqueue = nil)
    Gross::log.info "Starting up machine '#{@name}'"
    @tasks.each do |t|
        t.up if @tasks[t.id].deps.empty?
    end
    while true
        msg = @queue.pop
        case msg.type
        when :up
            up msg.id, extqueue
        when :down
            if @tasks.all? { |t| t.up? }
                Gross::log.info "Some tasks down for machine '#{@name}'"
                extqueue << :down unless extqueue == nil
            end
            down msg.id, extqueue
            extqueue << :down_done
        when :downup
            Gross::log.debug "Starting DOWNUP on '#{@name}'"
            if @tasks.all? { |t| t.up? }
                Gross::log.info "Some tasks down for machine '#{@name}'"
                extqueue << :down unless extqueue == nil
            end
            down msg.id, extqueue
            Gross::log.debug "DOWN done, UP starting on '#{@name}'"
            @tasks[msg.id].up
            Gross::log.debug "Ending DOWNUP on '#{@name}'"
        when :exit
            @tasks.each_index { |id| down id, extqueue if @tasks[id].upped? }
            Gross::log.info "Machine '#{@name}' exited"
            return
        else
            Gross::log.error "Machine '#{@name}' received invalid message: #{msg}"
        end
    end
end

#set(variable, value = '', &block) ⇒ Object

Sets a variable to a value

Parameters:

  • variable (ContextCallable)

    Variable name, given as a ContextCallable

  • value (ContextCallable) (defaults to: '')

    Value of the variable, given as a ContextCallable


29
30
31
32
33
34
35
36
37
38
# File 'lib/gross/tasks/set.rb', line 29

def set(variable, value='', &block)
    varname, var = context_callable variable
    valname, val = context_callable(value, &block)

    add_task(
        name: "set '#{varname}' := '#{valname}'",
        instant: true,
        up: lambda { @context[var.call @context] = val.call @context }
    )
end