langis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ module Langis
2
+ module Engine
3
+
4
+ ##
5
+ # Instances of this class are executed when a message is pushed to
6
+ # the intake's EventMachine::Channel that it subscribed to. Its
7
+ # primary function is to safely execute the Langis sink
8
+ # (Rackish application) that it has been tasked to manage. To do
9
+ # this safely with performance considerations, it enqueues a
10
+ # Proc to be handled by the EvenMachine's deferred thread pool and
11
+ # protects the thread pool by wrapping the sink call with a rescue block.
12
+ # Any caught errors will result in an error message with the caught
13
+ # exception pushed into the given EventMachine error channel. All
14
+ # successful completions will push the returned Rackish result to the
15
+ # EventMachine success channel.
16
+ class EventMachineRunner
17
+
18
+ ##
19
+ #
20
+ # @param [#call] app The Rackish app to execute.
21
+ # @param [Hash] options ({})
22
+ # @option options [EventMachine::Channel] :success_channel (nil) The
23
+ # EventMachine::Channel instance to push the Rackish app's return
24
+ # results to. This happens when there are no errors raised.
25
+ # @option options [EventMachine::Channel] :error_channel (nil) The
26
+ # EventMachine::Channel instance to push error messages to when the
27
+ # runner catches an exception during the execution of the Rackish app.
28
+ # @option options [Object] :evm (EventMachine) Specify a different
29
+ # class/module to use when executing deferred calls. Mainly useful
30
+ # for unit testing, or if you want to run the app directly in
31
+ # the main thread instead of the deferred thread pool.
32
+ def initialize(app, options={})
33
+ @app = app
34
+ @success_channel = options[:success_channel]
35
+ @error_channel = options[:error_channel]
36
+ @evm = options[:evm] || EventMachine
37
+ @intake_name = options[:intake_name]
38
+ end
39
+
40
+ ##
41
+ # The method that is called in the EventMachine's main reactor thread
42
+ # whose job is to enqueue the main app code to be run by EventMachine's
43
+ # deferred thread pool.
44
+ #
45
+ # This method sets up the Rackish environment hash that is passed
46
+ # as the main input to the Rackish app; it sets up the Proc that will
47
+ # be actually executed in by the deferred thread pool. The proc
48
+ # protects the thread pool from exceptions, and pushes respective
49
+ # error and success results to given EventMachine channels.
50
+ #
51
+ # @param [Object] message The message that is getting pushed through
52
+ # the Langis pipes to their eventual handlers.
53
+ def call(message)
54
+ # Assign local variables to the proper apps, etc for readability.
55
+ app = @app
56
+ success_channel = @success_channel
57
+ error_channel = @error_channel
58
+ intake_name = @intake_name
59
+ # Enqueue the proc to be run in by the deferred thread pool.
60
+ @evm.defer(proc do
61
+ # Create the base environment that is understood by the Rackish apps.
62
+ env = {}
63
+ env[MESSAGE_TYPE_KEY] = message.message_type.to_s if(
64
+ message.respond_to? :message_type)
65
+ env[MESSAGE_KEY] = message
66
+ env[INTAKE_KEY] = intake_name
67
+ # Actually run the Rackish app, protected by a rescue block.
68
+ # Push the results to their respective channels when finished.
69
+ begin
70
+ results = app.call env
71
+ success_channel.push(results) if success_channel
72
+ rescue => e
73
+ # It was an error, so we have to create a Rackish response array.
74
+ # We push a SERVER_ERROR status along with an enhanced
75
+ # headers section: the exception and original message.
76
+ error_channel.push([
77
+ SERVER_ERROR,
78
+ env.merge({ X_EXCEPTION => e}),
79
+ ['']]) if error_channel
80
+ end
81
+ end)
82
+ end
83
+ end
84
+
85
+ ##
86
+ # And EventMachine based implementation of a Langis Engine. Its sole
87
+ # job is to take a pumped message into an intake and broadcast the same
88
+ # message to all of the intake's registered sinks. In essense these
89
+ # engines need to execute the sinks handler methods for each message.
90
+ #
91
+ # This class leverages EventMachine's features to easily do efficient
92
+ # publishing to the subscribers, and uses the EventMachineRunner
93
+ # to do the actual code execution.
94
+ #
95
+ # @see EventMachineRunner
96
+ class EventMachineEngine
97
+
98
+ ##
99
+ # @param [Hash{String=>Array<#call>}] intakes The mapping of intake
100
+ # names to the list of Rackish applications (sinks) that subscribed
101
+ # to the given intake.
102
+ # @option options [Object] :evm (EventMachine) Specify a different
103
+ # class/module to use when executing deferred calls. Mainly useful
104
+ # for unit testing, or if you want to run the app directly in
105
+ # the main thread instead of the deferred thread pool.
106
+ # @option options [Class] :evm_channel (EventMachine::Channel) The
107
+ # channel class to instantiate as the underlying pub-sub engine. This
108
+ # is useful for unittesting, or if you want to implement a
109
+ # non-EventMachine pub-sub mechanism.
110
+ def initialize(intakes, options={})
111
+ evm_channel_class = options[:evm_channel] || EventMachine::Channel
112
+ @intake_channels = {}
113
+ intakes.each do |intake_name, apps|
114
+ runner_options = {
115
+ :success_channel => options[:success_channel],
116
+ :error_channel => options[:error_channel],
117
+ :evm => options[:evm],
118
+ :intake_name => intake_name
119
+ }
120
+ @intake_channels[intake_name] = channel = evm_channel_class.new
121
+ apps.each do |app|
122
+ channel.subscribe EventMachineRunner.new(app, runner_options)
123
+ end
124
+ end
125
+ end
126
+
127
+ ##
128
+ # Publishes a message into the Langis publish-subscribe bus.
129
+ #
130
+ # @overload pump(message)
131
+ # Publishes the message to the :default intake.
132
+ # @param [Object] message The message to publish.
133
+ # @overload pump(message, ...)
134
+ # Publishes the message to the given list of intakes.
135
+ # @param [Object] message The message to publish.
136
+ # @param [#to_s] ... Publish the message to these listed intakes
137
+ def pump(message, *intakes)
138
+ intakes.unshift :default if intakes.empty?
139
+ intakes.each do |name|
140
+ channel = @intake_channels[name.to_s]
141
+ channel.push(message) if channel
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,135 @@
1
+ module Langis
2
+ module Middleware
3
+
4
+ ##
5
+ # Middleware class that modifies the Rackish input environment by
6
+ # transforming a specific value in the environment before passing
7
+ # it along to the rest of the Rackish application chain.
8
+ #
9
+ # Some useful applications of this transform:
10
+ # * Serialization or Deserialization of input data.
11
+ # * Filter or modify the message; to explicitly whitelist the list of
12
+ # properties in the message that may be exposed to a service or
13
+ # third party.
14
+ class EnvFieldTransform
15
+
16
+ ##
17
+ #
18
+ # @param app The Rackish Application for which this instance is acting as
19
+ # middleware.
20
+ # @option options [String] :key (Langis::MESSAGE_KEY) The hash key
21
+ # of the Rackish environment whose value is the object that we
22
+ # want to transform (transformation object).
23
+ # @option options [Symbol,String] :to_method (:to_json) The
24
+ # transformation object that will respond to the invokation of this
25
+ # method name. The return value of that method will replace the
26
+ # original transformation object in the Rackish environment as
27
+ # the environment is passed on to the rest of the Rackish app chain.
28
+ # @option options [Array,Object] :to_args ([]) The parameter or
29
+ # list of parameters to pass to the transformation method.
30
+ def initialize(app, options={})
31
+ @app = app
32
+ @to_method = options[:to_method] || :to_json
33
+ @to_args = options[:to_args] || []
34
+ @to_args = [@to_args] unless @to_args.is_a? Array
35
+ @key = options[:key] || MESSAGE_KEY
36
+ end
37
+
38
+ ##
39
+ # Executes the object transformation, and invokes the rest of the
40
+ # Rackish app chain.
41
+ #
42
+ # @param [Hash] env The input Rackish Environment.
43
+ # @return [Array<Integer,Hash,#each>] The return of the proxied Rackish
44
+ # application chain.
45
+ def call(env)
46
+ item = env[@key].send @to_method, *@to_args
47
+ return @app.call env.merge({ @key => item })
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Middleware that adds an Array of values to the Rackish Environment
53
+ # input. This array of values is created by calling callables using the
54
+ # said Rackish Environment as input, and from static strings.
55
+ #
56
+ # The following example creates an Array of size two, and places it
57
+ # into the Rackish Environment key identified by 'my_key'. The first
58
+ # item in the Array is the static string, "Hello World". The second value
59
+ # is whatever was in the Rackish Environment under the key, "name".
60
+ #
61
+ # use Parameterizer,
62
+ # 'Hello World',
63
+ # lambda { |env| env['name'] },
64
+ # :env_key => 'my_key'
65
+ #
66
+ class Parameterizer
67
+
68
+ ##
69
+ # @param [#call] app The next link in the Rackish Application chain.
70
+ # @param [String,#call] *args The list of new parameters that the
71
+ # Parameterizer middleware creates. String values are used as is,
72
+ # and callable objects are executed with the input Rackish Environment
73
+ # as the first parameter.
74
+ # @option options [String] :env_key (::Langis::MESSAGE_KEY)
75
+ def initialize(app, *args)
76
+ @app = app
77
+ @options = args.last.kind_of?(Hash) ? args.pop : {}
78
+ @args = args
79
+ @env_key = @options[:env_key] || MESSAGE_KEY
80
+ end
81
+
82
+ ##
83
+ # The main method of the Parameterizer middleware.
84
+ #
85
+ # @param [Hash] env The input Rackish Environment.
86
+ def call(env={})
87
+ new_args = @args.map do |value|
88
+ value.respond_to?(:call) ? value.call(env) : value
89
+ end
90
+ new_env = {}.update(env)
91
+ new_env[@env_key] = new_args
92
+ return @app.call new_env
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Middleware to only continue execution of the Rackish application chain
98
+ # if the input environment's Langis::MESSAGE_TYPE_KEY is set to a value
99
+ # that has been whitelisted.
100
+ class MessageTypeFilter
101
+
102
+ ##
103
+ #
104
+ # @param app The Rackish application chain to front.
105
+ # @param [#to_s] ... The whitelist of message types to allow pass.
106
+ def initialize(app, *args)
107
+ @app = app
108
+ @message_types = args.map { |message_type| message_type.to_s }
109
+ end
110
+
111
+ ##
112
+ # Executes the filtering, and invokes the rest of the Rackish app chain
113
+ # if the message type is allowed.
114
+ #
115
+ # @param [Hash] env The Rackish input environment.
116
+ # @return [Array<Integer,Hash,#each>] The return of the proxied Rackish
117
+ # application chain, or an OK with the filter reason.
118
+ # @see Langis::X_FILTERED_BY
119
+ # @see Langis::X_FILTERED_TYPE
120
+ def call(env)
121
+ if @message_types.include? env[MESSAGE_TYPE_KEY]
122
+ return @app.call(env)
123
+ else
124
+ return [
125
+ OK,
126
+ {
127
+ X_FILTERED_BY => self.class.to_s,
128
+ X_FILTERED_TYPE => env[MESSAGE_TYPE_KEY].class
129
+ },
130
+ ['']]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,118 @@
1
+ module Langis
2
+ module Rackish
3
+
4
+ ##
5
+ # Error raised when RackishJob is asked to run an unregistered Rackish
6
+ # Application.
7
+ #
8
+ # @see RackishJob
9
+ class NotFoundError < LangisError
10
+ end
11
+
12
+ ##
13
+ # RackishJob is a dual DelayedJob-Resque job that is used to execute
14
+ # Rackish applications, or Rack applications that are robust against
15
+ # non-conformant "Rack Environments", in the background.
16
+ # Rackish Applications are created and registered with this RackishJob
17
+ # class. Each registration is associated with an app_key that is well
18
+ # known to any component that wants to execute that particular Rackish
19
+ # Application. Client components then queue up this job class with
20
+ # the app_key and the input hash for that application.
21
+ #
22
+ # Notes
23
+ # * This class does not provide a compliant Rack specified environment
24
+ # to the Rackish applications it calls. Prepend middleware that
25
+ # provides such an environment to the application chain if required.
26
+ #
27
+ # For example, to queue up a RackishJob using DelayedJob:
28
+ # Delayed::Job.enqueue Langis::Rackish::RackishJob.new(
29
+ # 'my_app',
30
+ # {
31
+ # 'input_key' => 'value'
32
+ # })
33
+ #
34
+ # For example, to queue up a RackishJob using Resque:
35
+ # Resque.enqueue(
36
+ # Langis::Rackish::RackishJob,
37
+ # 'my_app',
38
+ # {
39
+ # 'input_key' => 'value'
40
+ # })
41
+ #
42
+ # The my_app job may be registered in the worker process as follows:
43
+ # Langis::Rackish::RackishJob.register_rackish_app(
44
+ # 'my_app',
45
+ # lambda { |env|
46
+ # # Do something
47
+ # })
48
+ #
49
+ class RackishJob < Struct.new(:app_key, :env)
50
+ class << self
51
+
52
+ ##
53
+ # Registers a Rackish Application under a given name so it can be
54
+ # executed by the RackishJob class via the DelayedJob or Resque
55
+ # background job libraries.
56
+ #
57
+ # For example, the following can be found in a Rails initializer.
58
+ # my_app = Rack::Builder.app do
59
+ # run MyApp
60
+ # end
61
+ # RackishJob.register 'my_app', my_app
62
+ #
63
+ # @param [String] app_key The name used to lookup which Rackish
64
+ # application to call.
65
+ # @param [#call] app The Rackish Application to call for the requested
66
+ # app_key.
67
+ # @return [#call] The Rackish Application passed in is returned back.
68
+ def register_rackish_app(app_key, app)
69
+ @apps ||= {}
70
+ @apps[app_key.to_s] = app
71
+ end
72
+
73
+ ##
74
+ # Acts as the Resque starting point.
75
+ #
76
+ # For example, the following can be used to execute the 'my_app'
77
+ # Rackish application using Resque from an ActiveRecord callback:
78
+ # def after_create(record)
79
+ # Resque.enqueue RackishJob, 'my_app', { 'my.data' => record.id }
80
+ # end
81
+ #
82
+ # @param [String] app_key The registered application's name that
83
+ # is to be called with the given env input.
84
+ # @param [Hash] env The Rackish input environment. This is the input
85
+ # that should be relevant to the called app. There is no guarantee
86
+ # that this environment hash is a fully compliant Rack environment.
87
+ # @raise [RackishAppNotFoundError] Signals that the given app_key
88
+ # was not registered with RackishJob. See DelayedJob and Resque
89
+ # documentation to understand how to ignore or handle raised
90
+ # exceptions for retry.
91
+ def perform(app_key=nil, env={})
92
+ app = @apps[app_key]
93
+ if app.respond_to? :call
94
+ app.call env
95
+ else
96
+ raise NotFoundError.new "#{app_key} not found"
97
+ end
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Acts as the DelayedJob starting point. All this does is relays the
103
+ # call to the Resque starting point.
104
+ #
105
+ # For example, the following can be used to execute the 'my_app'
106
+ # Rackish application using DelayedJob from an ActiveRecord callback:
107
+ # def after_create(record)
108
+ # Delayed::Job.enqueue(
109
+ # RackishJob.new('my_app', { 'my.data' => record.id }))
110
+ # end
111
+ #
112
+ # @see RackishJob.perform
113
+ def perform
114
+ self.class.perform(app_key.to_s, env || {})
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,138 @@
1
+ module Langis
2
+
3
+ ##
4
+ # Predefined sinks, destinations for a message pumped into the Langis Engines.
5
+ module Sinks
6
+
7
+ ##
8
+ # The header key whose value is the DelayedJob enqueue result.
9
+ DELAYED_JOB_RESULT_KEY = 'langis.sink.delayed_job.result'
10
+
11
+ ##
12
+ # Module function that creates the endpoint Rackish app that will
13
+ # enqueue a new instance of a DelayedJob job class with instantiation
14
+ # parameters extracted from the Rackish input environment.
15
+ #
16
+ # @param [Class] job_class The DelayedJob job class to enqueue.
17
+ # @option options [String] :env_key (Langis::MESSAGE_KEY) The Rackish
18
+ # input environment key whose value is passed to the given job_class
19
+ # constructor. If the value of this key is an array, then the elements
20
+ # of that array are passed as though they were individually specified.
21
+ # @option options [Integer] :priority (0) DelayedJob priority to be used
22
+ # for all jobs enqueued with this sink.
23
+ # @option options [Time] :run_at (nil) DelayedJob run_at to be used for
24
+ # all jobs enqueued with this sink.
25
+ # @option options [Symbol] :transform (nil) method to call on the object
26
+ # passed in via :env_key if env_key responds to it. The returned Array's
27
+ # elements becomes the initializer argument(s) of the delayed job.
28
+ # If the return value is anything but an array, then that value is
29
+ # passed on; even explicit nils are passed on to the job_class#new.
30
+ # Note that DelayedJob serializes these parameters in Yaml.
31
+ # @option options [Array] :transform_args ([]) arguments to pass to
32
+ # the transform call.
33
+ # @return [Array<Integer,Hash,#each>] A simple OK return with the header
34
+ # hash that contains the delayed job enqueue result.
35
+ def delayed_job(job_class, options={})
36
+ priority = options[:priority] || 0
37
+ run_at = options[:run_at]
38
+ env_key = options[:env_key] || MESSAGE_KEY
39
+ xform = options[:transform]
40
+ xform_args = options[:transform_args] || []
41
+ xform_args = [xform_args] unless xform_args.is_a? Array
42
+ lambda { |env|
43
+ message = env[env_key]
44
+ if xform.is_a? Symbol and message.respond_to? xform
45
+ args = message.send(xform, *xform_args)
46
+ else
47
+ args = message
48
+ end
49
+ args = [args] unless args.is_a? Array
50
+ result = Delayed::Job.enqueue job_class.new(*args), priority, run_at
51
+ return [OK, { DELAYED_JOB_RESULT_KEY => result }, ['']]
52
+ }
53
+ end
54
+ module_function :delayed_job
55
+
56
+ ##
57
+ # The header key whose value is the Redis rpush result.
58
+ REDIS_RESULT_KEY = 'langis.sink.redis.result'
59
+
60
+ ##
61
+ # Module function that creates the endpoint Rackish app that will
62
+ # rpush an input environment's value into a list stored in a Redis
63
+ # database.
64
+ #
65
+ # @param [Object] connection The redis database connection.
66
+ # @param [String] key The index key of the list in the Redis database.
67
+ # @option options [String] :env_key (Langis::MESSAGE_KEY) The Rackish
68
+ # input environment key whose value is pushed onto the end of the
69
+ # Redis key's list.
70
+ # @option options [Symbol] :transform (nil) method to call on the object
71
+ # passed in via :env_key if env_key responds to it. The returned value
72
+ # is saved (after a #to_s by the Redis client) to the Redis database.
73
+ # @option options [Array] :transform_args ([]) arguments to pass to
74
+ # the transform call.
75
+ # @return [Array<Integer,Hash,#each>] A simple OK return with the header
76
+ # hash that contains the Redis#rpush result.
77
+ def redis(connection, key, options={})
78
+ env_key = options[:env_key] || MESSAGE_KEY
79
+ xform = options[:transform]
80
+ xform_args = options[:transform_args] || []
81
+ xform_args = [xform_args] unless xform_args.is_a? Array
82
+ lambda { |env|
83
+ message = env[env_key]
84
+ if xform.is_a? Symbol and message.respond_to? xform
85
+ message = message.send(xform, *xform_args)
86
+ end
87
+ result = connection.rpush key, message
88
+ return [OK, { REDIS_RESULT_KEY => result }, ['']]
89
+ }
90
+ end
91
+ module_function :redis
92
+
93
+ ##
94
+ # The header key whose value is the Resque enqueue result.
95
+ RESQUE_RESULT_KEY = 'langis.sink.resque.result'
96
+
97
+ ##
98
+ # Module function that creates the endpoint Rackish app that will
99
+ # rpush an input environment's value into a list stored in a Redis
100
+ # database.
101
+ #
102
+ # @param [Class] job_class The Resque job class for which we want
103
+ # to enqueue the message.
104
+ # @option options [String] :env_key (Langis::MESSAGE_KEY) The Rackish
105
+ # input environment key whose value is passed as the input arguments
106
+ # to the actual execution of the Resque job. The found value can be
107
+ # an Array, in which case the elements will be used as the execution
108
+ # parameters of the given job.
109
+ # @option options [Symbol] :transform (nil) method to call on the object
110
+ # passed in via :env_key if env_key responds to it. The returned Array's
111
+ # elements becomes the perform argument(s) of the Resque job.
112
+ # If the return value is anything but an array, then that value is
113
+ # passed on; even explicit nils are passed as the perform argument.
114
+ # Note that these Resque arguments are serialized via #to_json
115
+ # @option options [Array] :transform_args ([]) arguments to pass to
116
+ # the transform call.
117
+ # @return [Array<Integer,Hash,#each>] A simple OK return with the header
118
+ # hash that contains the Resque enqueue result.
119
+ def resque(job_class, options={})
120
+ env_key = options[:env_key] || MESSAGE_KEY
121
+ xform = options[:transform]
122
+ xform_args = options[:transform_args] || []
123
+ xform_args = [xform_args] unless xform_args.is_a? Array
124
+ lambda { |env|
125
+ message = env[env_key]
126
+ if xform.is_a? Symbol and message.respond_to? xform
127
+ args = message.send(xform, *xform_args)
128
+ else
129
+ args = message
130
+ end
131
+ args = [args] unless args.is_a? Array
132
+ result = Resque.enqueue job_class, *args
133
+ return [OK, { RESQUE_RESULT_KEY => result }, ['']]
134
+ }
135
+ end
136
+ module_function :resque
137
+ end
138
+ end