catena 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a115e71f17148d94f71b32657f92812dd5ce8f15
4
+ data.tar.gz: a9ff15140beffd48598c7d8a30ab8a97a56da7ff
5
+ SHA512:
6
+ metadata.gz: 97e499b07b87f98476c024828be4d0cdcdb32b3e3612f6687e2f6196ba9880a8a258dac53a89f7eebf5c1215769d7aa6b568538a8136189783f770c740b9279b
7
+ data.tar.gz: 4867fe0ba6e70df362cc76a9a7fc168dc86b7abb325cb25cbf15031a0497ed612fa2c6d12869495d766c5ca6fb907128df314f06ee4d2c25d65fe6fc0a609b56
@@ -0,0 +1,133 @@
1
+ require 'funkify'
2
+
3
+ module Catena
4
+ module Lang
5
+ include Funkify
6
+
7
+ # we add the class methods to the base class so they don't have to.
8
+ def self.included(base_mod)
9
+ base_mod.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def def_task(task_name, &block)
14
+ self.class_eval do
15
+ # define the language func that creates bind task nodes
16
+ define_method(task_name) do |*args|
17
+ bind(__method__, *args)
18
+ end
19
+
20
+ callback_name = Lang.func_name_to_callback(task_name)
21
+ define_method(callback_name, &block)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Helper functions
27
+
28
+ def self.callback_to_func_name(callback_name)
29
+ # strip the "__"
30
+ callback_name[2..-1]
31
+ end
32
+
33
+ def self.func_name_to_callback(func_name)
34
+ "__#{func_name}"
35
+ end
36
+
37
+ # basic tasks and their composition that return task nodes
38
+
39
+ def succeed(value)
40
+ {
41
+ "type" => "succeed",
42
+ "value" => value
43
+ }
44
+ end
45
+
46
+ def failure(error)
47
+ {
48
+ "type" => "failure",
49
+ "error" => error
50
+ }
51
+ end
52
+
53
+ # bind(callback_name, arg1, arg2, ...)
54
+ def bind(*args)
55
+ raise "Need at least callback_name" if args.length < 1
56
+ func_name = args[0]
57
+ func_args = args[1..-1] || []
58
+ {
59
+ "type" => "binding",
60
+ "callback_name" => Lang.func_name_to_callback(func_name),
61
+ "callback_args" => func_args,
62
+ "cancel" => nil,
63
+ }
64
+ end
65
+
66
+ auto_curry def and_then(bind_efx, efx_a)
67
+ binding_callback = bind_efx.is_a?(Symbol) ? bind(bind_efx) : bind_efx
68
+ raise "bind_efx needs to be a binding" if binding_callback["type"] != "binding"
69
+
70
+ {
71
+ "type" => "and_then",
72
+ "side_effect" => efx_a,
73
+ "binding_callback" => binding_callback,
74
+ }
75
+ end
76
+
77
+ auto_curry def on_error(bind_efx, efx_a)
78
+ binding_callback = bind_efx.is_a?(Symbol) ? bind(bind_efx) : bind_efx
79
+ raise "bind_efx needs to be a binding" if binding_callback["type"] != "binding"
80
+
81
+ {
82
+ "type" => "on_error",
83
+ "side_effect" => efx_a,
84
+ "binding_callback" => binding_callback,
85
+ }
86
+ end
87
+
88
+ # TODO We have #map2 to fan-in, but we don't have something that fans out
89
+ # example. We create an application space, but then need to upload two
90
+ # ml files to the space, and use the results to map into create_flask_app
91
+
92
+ auto_curry def map2(bind_efx, efx_b, efx_a)
93
+ binding_callback = bind_efx.is_a?(Symbol) ? bind(bind_efx) : bind_efx
94
+ raise "bind_efx needs to be a binding" if binding_callback["type"] != "binding"
95
+
96
+ pass(efx_a) >=
97
+ and_then(bind(:map2_a, binding_callback, efx_b))
98
+ end
99
+
100
+ # This only works because we're filling in all other args except val_a
101
+ auto_curry def __map2_a(bind_efx, efx_b, val_a, evaluator)
102
+ new_efx = pass(efx_b) >=
103
+ and_then(bind(:map2_b, bind_efx, val_a))
104
+
105
+ evaluator.call(new_efx)
106
+ end
107
+
108
+ auto_curry def __map2_b(bind_efx, val_a, val_b, evaluator)
109
+ # update binding to have val_a and val_b as arguments
110
+ func_name = Lang.callback_to_func_name(bind_efx["callback_name"])
111
+ args = bind_efx["callback_args"] + [val_a, val_b]
112
+ new_efx = bind(func_name, *args)
113
+
114
+ evaluator.call(new_efx)
115
+ end
116
+
117
+ auto_curry def smap(bind_efx, efxs)
118
+ self.send("map#{efxs.length}", bind_efx, *efxs.reverse)
119
+ end
120
+
121
+ # Unused. Sample usage
122
+ # pass([store_to_cloud("arch.hdf5"), store_to_cloud("model.hdf5")]) >=
123
+ # pmap(:tag_docker_image)
124
+ auto_curry def pmap(callback_name, efxs)
125
+ {
126
+ "type" => "pmap",
127
+ "side_effects" => efxs,
128
+ "serialized_callback" => "__#{callback_name}",
129
+ }
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,157 @@
1
+ require_relative 'lang'
2
+ require 'sidekiq'
3
+
4
+ module Catena
5
+ class Scheduler
6
+ include Lang
7
+ include Sidekiq::Worker
8
+
9
+ MAX_STEPS = 10
10
+
11
+ def perform(efx, stack)
12
+ step(efx, stack, 0)
13
+ end
14
+
15
+ def step(efx, stack, steps)
16
+ if (steps > MAX_STEPS)
17
+ logger.warn "Exceeded MAX STEPS. Stowing efx #{efx}"
18
+ enqueue(efx, stack)
19
+ steps
20
+ else
21
+ # TODO if not use >= after pass, will have efx["type"] is nil, need error message
22
+ # TODO if has trailing "|" in chain, it'll subsume the evaluator call, and return nil
23
+ # TODO also need error message when argument length mismatches
24
+ logger.debug "EFX: #{efx.inspect}"
25
+ send("step_#{efx["type"]}", efx, stack, steps)
26
+ end
27
+ end
28
+
29
+ def step_succeed(efx, stack, steps)
30
+ logger.debug "Processing succeed: #{efx}. Stack.len = #{stack.length}"
31
+ new_stack = flush("on_error", stack)
32
+ logger.debug " Flushed on_error. Stack.len = #{new_stack.length}"
33
+
34
+ if new_stack.empty?
35
+ return steps
36
+ else
37
+ and_then_node = new_stack.pop()
38
+ callback_node = and_then_node["binding_callback"] # the callback node is type binding
39
+ logger.debug " Popped #{callback_node}. Stack.len = #{new_stack.length}"
40
+
41
+ new_efx = chain(callback_node, efx["value"])
42
+ raise "step_succeed new_efx is nil. succeed value: #{efx["value"]}" if new_efx.nil?
43
+ step(new_efx, new_stack, steps + 1)
44
+ end
45
+ end
46
+
47
+ def step_failure(efx, stack, steps)
48
+ logger.info "Processing failure: #{efx}. Stack.len = #{stack.length}"
49
+ new_stack = flush("and_then", stack)
50
+ #puts " Flushed and_then. Stack.len = #{new_stack.length}"
51
+
52
+ if new_stack.empty?
53
+ return steps
54
+ else
55
+ on_error_node = new_stack.pop()
56
+ callback_node = on_error_node["binding_callback"]
57
+ logger.debug " Popped #{callback_node}. Stack.len = #{new_stack.length}"
58
+
59
+ new_efx = chain(callback_node, efx["error"])
60
+ raise "step_failure new_efx is nil. failure value: #{efx["error"]}" if new_efx.nil?
61
+ step(new_efx, new_stack, steps + 1)
62
+ end
63
+ end
64
+
65
+ def step_binding(efx, stack, steps)
66
+ # TODO canceling the entire process should happen at binding
67
+ logger.info "Processing binding of #{efx["callback_name"]}"
68
+
69
+ # FIXME shouldn't need to know explicitly the tasks are on Deployment class
70
+
71
+ callback = find_callback(efx["callback_name"])
72
+ args = efx["callback_args"] + [evaluator(stack)]
73
+ logger.debug " Calling '#{efx["callback_name"]}' with args: #{args.inspect}"
74
+
75
+ # FIXME check the arity and note if we're short?
76
+ # if we're at the end, and we're short on arguments, it'll happyly execute,
77
+ # return a lambda, and silently finish
78
+ callback.call(*args)
79
+
80
+ return steps + 1
81
+ end
82
+
83
+ def step_and_then(efx, stack, steps)
84
+ new_stack = stack.push(efx)
85
+ logger.info "Processing and_then. Stack.len = #{new_stack.length}"
86
+
87
+ raise "step_and_then new_efx is nil" if efx["side_effect"].nil?
88
+ step(efx["side_effect"], new_stack, steps + 1)
89
+ end
90
+
91
+ def step_on_error(efx, stack, steps)
92
+ logger.warn "Processing on_error...not implemented"
93
+ return steps
94
+ end
95
+
96
+ def step_pmap(efx, stack, steps)
97
+ logger.warn "Processing parallel map...not implemented"
98
+ return steps
99
+ end
100
+
101
+ #######################
102
+
103
+ private
104
+
105
+ # TODO should raise if callback isn't found
106
+ def find_callback(name)
107
+ mod_with_callback = Catena.config.modules.find do |mod|
108
+ mod = mod.is_a?(String) ? class_from_name(mod) : mod
109
+ mod.respond_to?(name)
110
+ end
111
+ return mod_with_callback.method(name)
112
+ end
113
+
114
+ def class_from_name()
115
+ mod_name.split("::").inject(Object) do |mod, class_name|
116
+ mod.const_get(class_name)
117
+ end
118
+ end
119
+
120
+ def enqueue(efx, stack)
121
+ logger.debug "Enqueued bound_efx: #{efx}"
122
+ Scheduler.perform_async(efx, stack)
123
+ end
124
+
125
+ def flush(node_type, stack)
126
+ stack.reject { |node| node["type"] == node_type }
127
+ end
128
+
129
+ # create another binding with the added efx["value"], and
130
+ # send it through a new step.
131
+ # while we can use funkify to partially apply, it doesn't work here, because
132
+ # we need to serialize it, and so we delay execution by creating another bind
133
+ # step through.
134
+ #
135
+ # same as calling the func that returns bind in Task
136
+ # TODO should use that so don't have to include Interpreter?
137
+ # But then still have to solve the problem of knowing about Deployment in step_binding
138
+ # TODO maybe just destructively update binding_callbcak with new value in args?
139
+ def chain(callback_node, value_or_error)
140
+ func_name = Lang.callback_to_func_name(callback_node["callback_name"])
141
+ args = callback_node["callback_args"] + [value_or_error]
142
+ return bind(func_name, *args)
143
+ end
144
+
145
+ def evaluator(stack)
146
+ lambda { |result_efx|
147
+ # FIXME need to check if result_efx is nil and then throw
148
+ # - Do you have a trailing "|"?
149
+ # FIXME need to check if result_efx is actually an efx
150
+ logger.info "Enqueuing result for evaluation"
151
+ logger.debug " result: #{result_efx}"
152
+ enqueue(result_efx, stack)
153
+ }
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,14 @@
1
+
2
+ module Catena
3
+
4
+ module Task
5
+ include Lang
6
+ include Funkify
7
+
8
+ auto_curry
9
+
10
+ # TODO move standard tasks here.
11
+
12
+ end
13
+
14
+ end
data/lib/catena.rb ADDED
@@ -0,0 +1,28 @@
1
+ require_relative 'catena/scheduler'
2
+ require_relative 'catena/lang'
3
+ require 'forwardable'
4
+
5
+ module Catena
6
+ Configuration = Struct.new(:modules)
7
+
8
+ class << self
9
+ extend Forwardable
10
+
11
+ attr_reader :config
12
+
13
+ def configure(&block)
14
+ @config = Configuration.new if @config.nil?
15
+ block.call(@config)
16
+ end
17
+
18
+ def perform(task)
19
+ Catena::Scheduler.perform_async(task, [])
20
+ end
21
+
22
+ def perform_now(task)
23
+ # Need to use Scheduler's find_callback to then run it
24
+ end
25
+
26
+ end
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: catena
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Wil Chung
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Catena lets you write and compose background tasks in a flexible way
14
+ to model business processes
15
+ email: iamwil@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/catena.rb
21
+ - lib/catena/lang.rb
22
+ - lib/catena/scheduler.rb
23
+ - lib/catena/task.rb
24
+ homepage: https://github.com/iamwilhelm/catena
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.6.12
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Chainable background tasks
48
+ test_files: []