alchemy-flux 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|