alchemy-flux 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/alchemy-flux.rb +409 -0
- data/lib/alchemy-flux/flux_rack_handler.rb +159 -0
- data/spec/performance_spec.rb +64 -0
- data/spec/rack_handler_spec.rb +50 -0
- data/spec/resources_spec.rb +68 -0
- data/spec/service_spec.rb +448 -0
- data/spec/spec_helper.rb +7 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9e0b6e71da840d69a7c637a0cb924df1c63fcf53
|
4
|
+
data.tar.gz: cb5572b0ce8972212d500e7f53e3234f9f7659f0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eb865f0288c6b2c3de1f4947b4440b35727bc48d1bb7d8f3029cbcebf5b70784384f487924fd66ff7a7e25723528ef59c88d77c8078c89c5f05cf16dd8c6930e
|
7
|
+
data.tar.gz: 725f8ee5b7ce199d5ea70fdc913c60aff118f085f7d76085a175e16f42161c7d126eceb10b550ac1f08245bbf83f90bc0868cb81c2dd47972eb5ab9287112907
|
data/lib/alchemy-flux.rb
ADDED
@@ -0,0 +1,409 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'amqp'
|
3
|
+
require "uuidtools"
|
4
|
+
require 'msgpack'
|
5
|
+
|
6
|
+
require 'alchemy-flux/flux_rack_handler.rb'
|
7
|
+
|
8
|
+
# Alchemy Flux module
|
9
|
+
module AlchemyFlux
|
10
|
+
|
11
|
+
# Error created when a Service message times out
|
12
|
+
class TimeoutError < StandardError; end
|
13
|
+
|
14
|
+
# Error created when a Message is unable to be delivered to a service
|
15
|
+
class MessageNotDeliveredError < StandardError; end
|
16
|
+
|
17
|
+
# Error used by a service when they wish the calling message to be NACKed *dangerous*
|
18
|
+
class NAckError < StandardError; end
|
19
|
+
|
20
|
+
# An Alchemy Flux Service
|
21
|
+
class Service
|
22
|
+
|
23
|
+
# The current state of the Service, either *stopped* or *started*
|
24
|
+
attr_reader :state
|
25
|
+
|
26
|
+
# The outgoing message transactions
|
27
|
+
attr_reader :transactions
|
28
|
+
|
29
|
+
# The incoming number of messages being processed
|
30
|
+
attr_reader :processing_messages
|
31
|
+
|
32
|
+
# Generate a UUID string
|
33
|
+
def self.generateUUID
|
34
|
+
UUIDTools::UUID.random_create.to_i.to_s(16).ljust(32,'0')
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a AlchemyFlux service instance
|
38
|
+
#
|
39
|
+
# +name+ the name of the service being created
|
40
|
+
# +options+
|
41
|
+
#
|
42
|
+
def initialize(name, options = {}, &block)
|
43
|
+
@name = name
|
44
|
+
@options = {
|
45
|
+
ampq_uri: 'amqp://localhost',
|
46
|
+
prefetch: 20,
|
47
|
+
timeout: 1000,
|
48
|
+
threadpool_size: 500,
|
49
|
+
resource_paths: []
|
50
|
+
}.merge(options)
|
51
|
+
|
52
|
+
@service_fn = block || Proc.new { |message| "" }
|
53
|
+
|
54
|
+
@uuid = "#{@name}.#{AlchemyFlux::Service.generateUUID()}"
|
55
|
+
@transactions = {}
|
56
|
+
@processing_messages = 0
|
57
|
+
|
58
|
+
@response_queue_name = @uuid
|
59
|
+
@service_queue_name = @name
|
60
|
+
@state = :stopped
|
61
|
+
end
|
62
|
+
|
63
|
+
# overriding inspect
|
64
|
+
def inspect
|
65
|
+
to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# overriding to_s
|
69
|
+
def to_s
|
70
|
+
"AlchemyFlux::Service(#{@name.inspect}, #{@options.inspect})"
|
71
|
+
end
|
72
|
+
|
73
|
+
# LIFE CYCLE
|
74
|
+
|
75
|
+
|
76
|
+
# Start the EventMachine and AMQP connections for all Services
|
77
|
+
#
|
78
|
+
# The application has two or more threads
|
79
|
+
# 1. The Controller Thread (e.g. the rspec thread)
|
80
|
+
# 2. The EM Thread
|
81
|
+
# 3. The EM defer Threads
|
82
|
+
#
|
83
|
+
# When we start a Service we do it in a Thread so that it will not block the calling Thread
|
84
|
+
#
|
85
|
+
# When the FIRST Service is started EM.run initialises in that Thread
|
86
|
+
# When the second Service is initialises the block is executed in the new thread,
|
87
|
+
# but all the callbacks will be executed in the EM thread
|
88
|
+
#
|
89
|
+
def self.start(ampq_uri = 'amqp://localhost', threadpool_size=500)
|
90
|
+
return if EM.reactor_running?
|
91
|
+
start_blocker = Queue.new
|
92
|
+
Thread.new do
|
93
|
+
Thread.current["name"] = "EM Thread" if EM.reactor_thread?
|
94
|
+
Thread.current.abort_on_exception = true
|
95
|
+
EM.threadpool_size = threadpool_size
|
96
|
+
AMQP.start(ampq_uri) do |connection|
|
97
|
+
@@connection = connection
|
98
|
+
@@connection.on_error do |conn, connection_close|
|
99
|
+
message = "Channel exception: [#{connection_close.reply_code}] #{connection_close.reply_text}"
|
100
|
+
puts message
|
101
|
+
raise message
|
102
|
+
end
|
103
|
+
start_blocker << :unblock
|
104
|
+
end
|
105
|
+
end
|
106
|
+
start_blocker.pop
|
107
|
+
end
|
108
|
+
|
109
|
+
# Stop EventMachine and the
|
110
|
+
def self.stop
|
111
|
+
return if !EM.reactor_running?
|
112
|
+
stop_blocker = Queue.new
|
113
|
+
|
114
|
+
#last tick
|
115
|
+
AMQP.stop do
|
116
|
+
EM.stop_event_loop
|
117
|
+
stop_blocker << :unblock
|
118
|
+
end
|
119
|
+
stop_blocker.pop
|
120
|
+
sleep(0.05) # to ensure it finished
|
121
|
+
end
|
122
|
+
|
123
|
+
# start the service
|
124
|
+
def start
|
125
|
+
return if @state != :stopped
|
126
|
+
|
127
|
+
Service.start(@options[:ampq_uri], @options[:threadpool_size])
|
128
|
+
EM.run do
|
129
|
+
|
130
|
+
@channel = AMQP::Channel.new(@@connection)
|
131
|
+
|
132
|
+
@channel.on_error do |ch, channel_close|
|
133
|
+
message = "Channel exception: [#{channel_close.reply_code}] #{channel_close.reply_text}"
|
134
|
+
puts message
|
135
|
+
raise message
|
136
|
+
end
|
137
|
+
|
138
|
+
@channel.prefetch(@options[:prefetch])
|
139
|
+
@channel.auto_recovery = true
|
140
|
+
|
141
|
+
@service_queue = @channel.queue( @service_queue_name, {:durable => true})
|
142
|
+
@service_queue.subscribe({:ack => true}) do |metadata, payload|
|
143
|
+
payload = MessagePack.unpack(payload)
|
144
|
+
process_service_queue_message(metadata, payload)
|
145
|
+
end
|
146
|
+
|
147
|
+
response_queue = @channel.queue(@response_queue_name, {:exclusive => true, :auto_delete => true})
|
148
|
+
response_queue.subscribe({}) do |metadata, payload|
|
149
|
+
payload = MessagePack.unpack(payload)
|
150
|
+
process_response_queue_message(metadata, payload)
|
151
|
+
end
|
152
|
+
|
153
|
+
@channel.default_exchange.on_return do |basic_return, frame, payload|
|
154
|
+
payload = MessagePack.unpack(payload)
|
155
|
+
process_returned_message(basic_return, frame.properties, payload)
|
156
|
+
end
|
157
|
+
|
158
|
+
# RESOURCES HANDLE
|
159
|
+
@resources_exchange = @channel.topic("resources.exchange", {:durable => true})
|
160
|
+
@resources_exchange.on_return do |basic_return, frame, payload|
|
161
|
+
payload = MessagePack.unpack(payload)
|
162
|
+
process_returned_message(basic_return, frame.properties, payload)
|
163
|
+
end
|
164
|
+
|
165
|
+
bound_resources = 0
|
166
|
+
for resource_path in @options[:resource_paths]
|
167
|
+
binding_key = "#{path_to_routing_key(resource_path)}.#"
|
168
|
+
@service_queue.bind(@resources_exchange, :key => binding_key) {
|
169
|
+
bound_resources += 1
|
170
|
+
}
|
171
|
+
end
|
172
|
+
begin
|
173
|
+
# simple loop to wait for the resources to be bound
|
174
|
+
sleep(0.01)
|
175
|
+
end until bound_resources == @options[:resource_paths].length
|
176
|
+
|
177
|
+
@state = :started
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Stop the Service
|
182
|
+
#
|
183
|
+
# This method:
|
184
|
+
# * Stops receiving new messages
|
185
|
+
# * waits for processing incoming and outgoing messages to be completed
|
186
|
+
# * close the channel
|
187
|
+
def stop
|
188
|
+
return if @state != :started
|
189
|
+
# stop receiving new incoming messages
|
190
|
+
@service_queue.unsubscribe
|
191
|
+
# only stop the service if all incoming and outgoing messages are complete
|
192
|
+
decisecond_timeout = @options[:timeout]/100
|
193
|
+
waited_deciseconds = 0 # guarantee that this loop will stop
|
194
|
+
while (@transactions.length > 0 || @processing_messages > 0) && waited_deciseconds < decisecond_timeout
|
195
|
+
sleep(0.1) # wait a decisecond to check the incoming and outgoing messages again
|
196
|
+
waited_deciseconds += 1
|
197
|
+
end
|
198
|
+
|
199
|
+
@channel.close
|
200
|
+
@state = :stopped
|
201
|
+
end
|
202
|
+
|
203
|
+
# END OF LIFE CYCLE
|
204
|
+
|
205
|
+
|
206
|
+
|
207
|
+
private
|
208
|
+
# RECIEVING MESSAGES
|
209
|
+
|
210
|
+
# process messages on the service queue
|
211
|
+
def process_service_queue_message(metadata, payload)
|
212
|
+
|
213
|
+
service_to_reply_to = metadata.reply_to
|
214
|
+
message_replying_to = metadata.message_id
|
215
|
+
this_message_id = AlchemyFlux::Service.generateUUID()
|
216
|
+
delivery_tag = metadata.delivery_tag
|
217
|
+
interaction_id = payload['headers']['x-interaction-id']
|
218
|
+
|
219
|
+
operation = proc {
|
220
|
+
@processing_messages += 1
|
221
|
+
begin
|
222
|
+
response = @service_fn.call(payload)
|
223
|
+
{
|
224
|
+
'status_code' => response['status_code'] || 200,
|
225
|
+
'headers' => response['headers'] || { 'x-interaction-id' => interaction_id},
|
226
|
+
'body' => response['body'] || ""
|
227
|
+
}
|
228
|
+
rescue AlchemyFlux::NAckError => e
|
229
|
+
AlchemyFlux::NAckError
|
230
|
+
rescue Exception => e
|
231
|
+
puts "Service Fn Error " + e.inspect
|
232
|
+
|
233
|
+
# Returning Hoodoo formatted Error code (just in case service doesnt handle the error, it should!)
|
234
|
+
{
|
235
|
+
'status_code' => 500,
|
236
|
+
'headers' => {'Content-Type' => 'application/json; charset=utf-8'},
|
237
|
+
'body' => {
|
238
|
+
'kind' => "Errors",
|
239
|
+
'id' => AlchemyFlux::Service.generateUUID(),
|
240
|
+
'created_at' => Time.now.utc.iso8601,
|
241
|
+
'interaction_id' => interaction_id,
|
242
|
+
'errors' => [{'code' => 'platform.fault', 'message' => 'An unexpected error occurred'}]
|
243
|
+
}
|
244
|
+
}
|
245
|
+
end
|
246
|
+
}
|
247
|
+
|
248
|
+
callback = proc { |result|
|
249
|
+
|
250
|
+
if result == AlchemyFlux::NAckError
|
251
|
+
@service_queue.reject(delivery_tag)
|
252
|
+
else
|
253
|
+
options = {
|
254
|
+
:message_id => this_message_id,
|
255
|
+
:correlation_id => message_replying_to,
|
256
|
+
:type => 'http_response'
|
257
|
+
}
|
258
|
+
send_message(@channel.default_exchange, service_to_reply_to, result, options)
|
259
|
+
@processing_messages -= 1
|
260
|
+
@service_queue.acknowledge(delivery_tag)
|
261
|
+
end
|
262
|
+
}
|
263
|
+
|
264
|
+
EventMachine.defer(operation, callback)
|
265
|
+
end
|
266
|
+
|
267
|
+
# process a response message
|
268
|
+
#
|
269
|
+
# If a message is put on this services response queue
|
270
|
+
# its response will be pushed onto the blocking queue
|
271
|
+
def process_response_queue_message(metadata, payload)
|
272
|
+
response_queue = @transactions.delete metadata.correlation_id
|
273
|
+
response_queue << payload if response_queue
|
274
|
+
end
|
275
|
+
|
276
|
+
# process a returned message
|
277
|
+
#
|
278
|
+
# If a message is sent to a queue that cannot be found,
|
279
|
+
# rabbitmq returns that message to this method
|
280
|
+
def process_returned_message(basic_return, metadata, payload)
|
281
|
+
response_queue = @transactions.delete metadata[:message_id]
|
282
|
+
response_queue << MessageNotDeliveredError if response_queue
|
283
|
+
end
|
284
|
+
|
285
|
+
# END OF RECIEVING MESSAGES
|
286
|
+
|
287
|
+
public
|
288
|
+
|
289
|
+
# SENDING MESSAGES
|
290
|
+
|
291
|
+
# send a message to an exchange with routing key
|
292
|
+
#
|
293
|
+
# *exchange*:: A AMQP exchange
|
294
|
+
# *routing_key*:: The routing key to use
|
295
|
+
# *message*:: The message to be sent
|
296
|
+
# *options*:: The message options
|
297
|
+
def send_message(exchange, routing_key, message, options)
|
298
|
+
message_options = options.merge({:routing_key => routing_key})
|
299
|
+
message = MessagePack.pack(message)
|
300
|
+
EventMachine.next_tick do
|
301
|
+
exchange.publish message, message_options
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# send a message to a service
|
306
|
+
#
|
307
|
+
# *service_name*:: the name of the service
|
308
|
+
# *message*:: the message to be sent
|
309
|
+
#
|
310
|
+
# This method can optionally take a block which will be executed asynchronously and yielded the response
|
311
|
+
def send_message_to_service(service_name, message)
|
312
|
+
if block_given?
|
313
|
+
EventMachine.defer do
|
314
|
+
yield send_message_to_service(service_name, message)
|
315
|
+
end
|
316
|
+
else
|
317
|
+
send_HTTP_request_message(@channel.default_exchange, service_name, message)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# send a message to a resource
|
322
|
+
#
|
323
|
+
# *message*:: the message to be sent to the *path* in the message
|
324
|
+
#
|
325
|
+
# This method can optionally take a block which will be executed asynchronously and yielded the response
|
326
|
+
def send_message_to_resource(message)
|
327
|
+
routing_key = path_to_routing_key(message['path'])
|
328
|
+
if block_given?
|
329
|
+
EventMachine.defer do
|
330
|
+
yield send_message_to_resource(message)
|
331
|
+
end
|
332
|
+
else
|
333
|
+
send_HTTP_request_message(@resources_exchange, routing_key, message)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
|
338
|
+
private
|
339
|
+
|
340
|
+
# Takes a path and converts it into a routing key
|
341
|
+
#
|
342
|
+
# *path*:: path string
|
343
|
+
#
|
344
|
+
# For example, path '/test/path' will convert to routing key 'test.path'
|
345
|
+
def path_to_routing_key(path)
|
346
|
+
new_path = ""
|
347
|
+
path.split('').each_with_index do |c,i|
|
348
|
+
if c == '/' and i != 0 and i != path.length-1
|
349
|
+
new_path += '.'
|
350
|
+
elsif c != '/'
|
351
|
+
new_path += c
|
352
|
+
end
|
353
|
+
end
|
354
|
+
new_path
|
355
|
+
end
|
356
|
+
|
357
|
+
# send a HTTP message to an exchange with routing key
|
358
|
+
#
|
359
|
+
# *exchange*:: A AMQP exchange
|
360
|
+
# *routing_key*:: The routing key to use
|
361
|
+
# *message*:: The message to be sent
|
362
|
+
def send_HTTP_request_message(exchange, routing_key, message)
|
363
|
+
|
364
|
+
http_message = {
|
365
|
+
'session_id' => message['session_id'],
|
366
|
+
'scheme' => message['protocol'] || 'http',
|
367
|
+
'host' => message['hostname'] || 'localhost',
|
368
|
+
'port' => message['port'] || 8080,
|
369
|
+
'path' => message['path'] || "/",
|
370
|
+
'query' => message['query'] || {},
|
371
|
+
'verb' => message['verb'] || "GET",
|
372
|
+
'headers' => message['headers'] || {},
|
373
|
+
'body' => message['body'] || ""
|
374
|
+
}
|
375
|
+
|
376
|
+
if !http_message['headers']['x-interaction-id']
|
377
|
+
http_message['headers']['x-interaction-id'] = AlchemyFlux::Service.generateUUID()
|
378
|
+
end
|
379
|
+
|
380
|
+
message_id = AlchemyFlux::Service.generateUUID()
|
381
|
+
|
382
|
+
http_message_options = {
|
383
|
+
message_id: message_id,
|
384
|
+
type: 'http_request',
|
385
|
+
reply_to: @response_queue_name,
|
386
|
+
content_encoding: '8bit',
|
387
|
+
content_type: 'application/octet-stream',
|
388
|
+
expiration: @options[:timeout],
|
389
|
+
mandatory: true
|
390
|
+
}
|
391
|
+
|
392
|
+
response_queue = Queue.new
|
393
|
+
@transactions[message_id] = response_queue
|
394
|
+
|
395
|
+
send_message(exchange, routing_key, http_message, http_message_options)
|
396
|
+
|
397
|
+
EventMachine.add_timer(@options[:timeout]/1000.0) do
|
398
|
+
response_queue = @transactions.delete message_id
|
399
|
+
response_queue << TimeoutError if response_queue
|
400
|
+
end
|
401
|
+
|
402
|
+
response_queue.pop
|
403
|
+
end
|
404
|
+
|
405
|
+
# END OF SENDING MESSAGES
|
406
|
+
|
407
|
+
end
|
408
|
+
|
409
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# This file creates a Service that Talks Rack
|
2
|
+
require 'rack'
|
3
|
+
require 'alchemy-flux'
|
4
|
+
|
5
|
+
# The Rack namespace
|
6
|
+
module Rack
|
7
|
+
# The Rack Handlers namespace
|
8
|
+
module Handler
|
9
|
+
# Alchemy Rack handler
|
10
|
+
class AlchemyFlux
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
# Start the app server with the supplied Rack application and options
|
15
|
+
#
|
16
|
+
# +app+ [Rack Application] The Application to run.
|
17
|
+
# +options+ [Hash] The options to start the server with.
|
18
|
+
def self.run(app, options={})
|
19
|
+
start(app)
|
20
|
+
|
21
|
+
puts "Started #{@@service.inspect}"
|
22
|
+
|
23
|
+
Signal.trap("INT") do
|
24
|
+
puts "Stopping #{@@service.inspect}"
|
25
|
+
stop
|
26
|
+
end
|
27
|
+
|
28
|
+
Signal.trap("TERM") do
|
29
|
+
puts "Stopping #{@@service.inspect}"
|
30
|
+
stop
|
31
|
+
end
|
32
|
+
|
33
|
+
EM.reactor_thread.join
|
34
|
+
end
|
35
|
+
|
36
|
+
# start the service for rack
|
37
|
+
def self.start(app)
|
38
|
+
service_name = ENV['SERVICE_NAME']
|
39
|
+
raise RuntimeError.new("Require SERVICE_NAME environment variable") if !service_name
|
40
|
+
|
41
|
+
options = {
|
42
|
+
ampq_uri: ENV['AMQ_URI'] || 'amqp://localhost',
|
43
|
+
prefetch: ENV['PREFETCH'] || 20,
|
44
|
+
timeout: ENV['TIMEOUT'] || 30000,
|
45
|
+
threadpool_size: ENV['THREADPOOL_SIZE'] || 500,
|
46
|
+
resource_paths: (ENV['RESOURCE_PATHS'] || '').split(',')
|
47
|
+
}
|
48
|
+
|
49
|
+
if options[:prefetch] > options[:threadpool_size]
|
50
|
+
puts "WARNING: 'prefect' is greater than the available threads which may cause performance blocking problems"
|
51
|
+
end
|
52
|
+
|
53
|
+
@@service = ::AlchemyFlux::Service.new(service_name, options) do |message|
|
54
|
+
rack_env = create_rack_env(message)
|
55
|
+
|
56
|
+
# add Alchemy Service so the app may call other services
|
57
|
+
rack_env['alchemy.service'] = @@service
|
58
|
+
|
59
|
+
status, headers, body = app.call(rack_env)
|
60
|
+
|
61
|
+
# process the body into a single response string
|
62
|
+
body.close if body.respond_to?(:close)
|
63
|
+
response = ""
|
64
|
+
body.each { |part| response << part }
|
65
|
+
|
66
|
+
{
|
67
|
+
'status_code' => status,
|
68
|
+
'headers' => headers,
|
69
|
+
'body' => response
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
@@service.start
|
75
|
+
end
|
76
|
+
|
77
|
+
# stops the app service
|
78
|
+
def self.stop
|
79
|
+
@@service.stop
|
80
|
+
EM.stop
|
81
|
+
end
|
82
|
+
|
83
|
+
# create the environment hash to be sent to the app
|
84
|
+
def self.create_rack_env(message)
|
85
|
+
|
86
|
+
stream = StringIO.new(message['body'])
|
87
|
+
stream.set_encoding(Encoding::ASCII_8BIT)
|
88
|
+
|
89
|
+
|
90
|
+
# Full description of rack env http://www.rubydoc.info/github/rack/rack/master/file/SPEC
|
91
|
+
rack_env = {}
|
92
|
+
|
93
|
+
# CGI-like (adopted from PEP333) variables
|
94
|
+
|
95
|
+
# The HTTP request method, such as “GET” or “POST”
|
96
|
+
rack_env['REQUEST_METHOD'] = message['verb'].to_s.upcase
|
97
|
+
|
98
|
+
# This is an empty string to correspond with the “root” of the server.
|
99
|
+
rack_env['SCRIPT_NAME'] = ''
|
100
|
+
|
101
|
+
# The remainder of the request URL's “path”, designating the virtual “location” of the request's target within the application.
|
102
|
+
rack_env['PATH_INFO'] = message['path']
|
103
|
+
|
104
|
+
# The portion of the request URL that follows the ?, if any
|
105
|
+
rack_env['QUERY_STRING'] = Rack::Utils.build_query(message['query'])
|
106
|
+
|
107
|
+
# Used to complete the URL
|
108
|
+
rack_env['SERVER_NAME'] = message['host']
|
109
|
+
rack_env['SERVER_PORT'] = message['port'].to_s
|
110
|
+
|
111
|
+
|
112
|
+
# Headers are added to the rack env as described by RFC3875 https://www.ietf.org/rfc/rfc3875
|
113
|
+
if message['headers'].is_a? Hash
|
114
|
+
message['headers'].each do |name, value|
|
115
|
+
name = "HTTP_" + name.to_s.upcase.gsub(/[^A-Z0-9]/,'_')
|
116
|
+
rack_env[name] = value.to_s
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# The environment must not contain the keys HTTP_CONTENT_TYPE or HTTP_CONTENT_LENGTH (use the versions without HTTP_)
|
121
|
+
rack_env['CONTENT_TYPE'] = rack_env['HTTP_CONTENT_TYPE'] || 'application/octet-stream'
|
122
|
+
rack_env['CONTENT_LENGTH'] = rack_env['HTTP_CONTENT_LENGTH'] || stream.length.to_s
|
123
|
+
rack_env.delete('HTTP_CONTENT_TYPE')
|
124
|
+
rack_env.delete('HTTP_CONTENT_LENGTH')
|
125
|
+
|
126
|
+
|
127
|
+
# Rack-specific variables
|
128
|
+
|
129
|
+
# The Array representing this version of Rack See Rack::VERSION
|
130
|
+
rack_env['rack.version'] = Rack::VERSION
|
131
|
+
|
132
|
+
# http or https, depending on the request URL.
|
133
|
+
rack_env['rack.url_scheme'] = message['scheme']
|
134
|
+
|
135
|
+
# the input stream.
|
136
|
+
rack_env['rack.input'] = stream
|
137
|
+
|
138
|
+
# the error stream.
|
139
|
+
rack_env['rack.errors'] = STDERR
|
140
|
+
|
141
|
+
# true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
|
142
|
+
rack_env['rack.multithread'] = true
|
143
|
+
|
144
|
+
# true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
|
145
|
+
rack_env['rack.multiprocess'] = false
|
146
|
+
|
147
|
+
# true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process.
|
148
|
+
rack_env['rack.run_once'] = false
|
149
|
+
|
150
|
+
# present and true if the server supports connection hijacking.
|
151
|
+
rack_env['rack.hijack?'] = false
|
152
|
+
|
153
|
+
rack_env
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
register :alchemy, Rack::Handler::AlchemyFlux
|
158
|
+
end
|
159
|
+
end
|