langis 0.1.0

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.
@@ -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