thick 0.0.4-java

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.
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## 0.4 (xy.xz.2012)
2
+
3
+ * make gem Java specific
4
+ * copy-less request body container
5
+ * async responses
6
+
7
+ ## 0.3 (26.3.2012)
8
+
9
+ Update Netty to 3.4.0.Alpha2
10
+
11
+ ## 0.2 (26.3.2012)
12
+
13
+ First public version
14
+
15
+ ## 0.1 (26.3.2012)
16
+
17
+ Claim the gem ;)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Portions copyright (c) 2012 Marek Jelen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Thick
2
+
3
+ Thick is very lightweight web server for JRuby based on excellent Netty library.
4
+
5
+ ## Status
6
+
7
+ Works on several Sinatra and Rails applications. No real testing, yet.
8
+
9
+ ## Speed
10
+
11
+ see PERFORMANCE.md
12
+
13
+ ## Interfaces
14
+
15
+ Thick provides interfaces to control it's behaviour.
16
+
17
+ ### Asynchronous responses
18
+
19
+ Asynchronous responses provide functionality to stream response to the client by chunks.
20
+
21
+ The most basic API looks
22
+
23
+ env['thick.async'].async!(*params, &block)
24
+
25
+ the application has to respond in a standard manner
26
+
27
+ [status, headers, body]
28
+
29
+ the response will be processed and send to the client, however the connection is not closed, instead the block is
30
+ executed with parameters passed as params. From the block the application can stream data to the client
31
+
32
+ env['thick.async'].call(chunk)
33
+
34
+ after the response is completed the application has to close the connection
35
+
36
+ env['thick.async'].close
37
+
38
+ In more complex scenarios the params and block may be omitted. It's up to the application to wait till the response
39
+ header is sent to the client and stream it's data. To simplify the signalization the application may check if it's safe
40
+ to send it's data by.
41
+
42
+ env['thick.async'].ready?
43
+
44
+ if the method's response is positive it's safe to stream the data.
45
+
46
+ In the most complex cases it might be necessary to completely bypass default HTTP response generated by the server. In
47
+ those cases the application calls
48
+
49
+ env['thick.async'].custom!(*params, &block)
50
+
51
+ the method informs the server to not send any response on the connection and do not close the connection.
52
+ The contract is the same as with async! mechanism. The block with parameters params is triggered by the server when
53
+ it's safe to stream data. If the block is not given it's completely up to the application to stream the data.
54
+ The application may check whether the server considers safe to stream data by the same way as with async! response
55
+
56
+ env['thick.async'].ready?
57
+
58
+ In the case of custom responses it should be safe to stream right away without checking the server's opinion. Also with
59
+ custom responses it's not necessary to return standard rack response.
60
+
61
+ ## Starting
62
+
63
+ thick [-o <interface>] [-p <port>] [-E <environment>]
64
+
65
+ ## Installation
66
+
67
+ gem install thick
68
+
69
+ ## License
70
+
71
+ MIT ... let me know if that's a problem for you.
data/ROADMAP.md ADDED
@@ -0,0 +1,12 @@
1
+ ## Must to have
2
+
3
+ * SSL support
4
+ * Tests
5
+ * Code refactorings
6
+ * Optimalizations
7
+ * Replace StringIO
8
+
9
+ ## Nice to have
10
+
11
+ * SPDY support
12
+ * WebSockets support
data/bin/thick ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ require 'rubygems'
5
+ require 'rack'
6
+
7
+ $: << File.expand_path('../../lib', __FILE__)
8
+ require 'thick'
9
+
10
+ options = {}
11
+
12
+ OptionParser.new do |opts|
13
+
14
+ opts.banner = 'Usage: thick [options]'
15
+
16
+ opts.separator ''
17
+ opts.separator 'Ruby options:'
18
+
19
+ opts.separator ''
20
+ opts.separator 'Rack options:'
21
+
22
+ opts.on('-o', '--host HOST', 'listen on HOST (default: 0.0.0.0)') do |host|
23
+ options[:address] = host
24
+ end
25
+
26
+ opts.on('-p', '--port PORT', 'use PORT (default: 9292)') do |port|
27
+ options[:port] = Integer(port)
28
+ end
29
+
30
+ opts.on('-E', '--env ENVIRONMENT', 'use ENVIRONMENT for defaults (default: development)') do |e|
31
+ options[:environment] = e
32
+ end
33
+
34
+ end.parse!
35
+
36
+ options[:address] ||= '0.0.0.0'
37
+ options[:port] ||= 9292
38
+ options[:environment] ||= 'development'
39
+
40
+ ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] ||= options[:environment]
41
+
42
+ puts "Loading app #{options.inspect}"
43
+ app = Rack::Builder.parse_file('config.ru')
44
+
45
+ puts "Starting server"
46
+ Thick.start(app[0], options)
Binary file
@@ -0,0 +1,53 @@
1
+ module Thick
2
+
3
+ class AsyncResponse
4
+
5
+ def initialize(context)
6
+ @context = context
7
+ @ready = false
8
+ @custom = false
9
+ @async = false
10
+ @block = nil
11
+ @params = []
12
+ end
13
+
14
+ def call(chunk)
15
+ @context.channel.write(Thick::Java::ChannelBuffers.copiedBuffer(chunk.to_s, Thick::Java::CharsetUtil::UTF_8))
16
+ end
17
+
18
+ def close
19
+ @context.channel.write(Thick::Java::ChannelBuffers::EMPTY_BUFFER).addListener(Thick::Java::ChannelFutureListener::CLOSE)
20
+ end
21
+
22
+ def ready!
23
+ @ready = true
24
+ @block.call(*@params) if @block
25
+ end
26
+
27
+ def ready?
28
+ @ready
29
+ end
30
+
31
+ def custom!(*params, &block)
32
+ @custom = true
33
+ @block = block
34
+ @params = params
35
+ end
36
+
37
+ def custom?
38
+ @custom
39
+ end
40
+
41
+ def async!(*params, &block)
42
+ @async = true
43
+ @block = block
44
+ @params = params
45
+ end
46
+
47
+ def async?
48
+ @async
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,42 @@
1
+ module Thick
2
+
3
+ class Buffer
4
+
5
+ def initialize
6
+ @buffers = []
7
+ @buffer = nil
8
+ @io = nil
9
+ end
10
+
11
+ def io
12
+ @buffer ||= Thick::Java::ChannelBuffers.wrapped_buffer(*@buffers)
13
+ @io ||= Thick::Java::ChannelBufferInputStream.new(@buffer).to_io.binmode
14
+ end
15
+
16
+ def write(data)
17
+ raise ArgumentError unless data.kind_of?(Thick::Java::ChannelBuffer)
18
+ @buffers << data
19
+ @buffer = nil
20
+ @io = nil
21
+ end
22
+
23
+ def gets
24
+ self.io.gets
25
+ end
26
+
27
+ def read(length = nil, buffer = nil)
28
+ buffer ? self.io.read(length, buffer) : self.io.read(length)
29
+ end
30
+
31
+ def each(&block)
32
+ self.io.each(&block)
33
+ end
34
+
35
+ def rewind
36
+ @buffer.reader_index(0) if @buffer
37
+ @io = nil
38
+ end
39
+
40
+ end
41
+
42
+ end
data/lib/thick/java.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Thick
2
+ module Java
3
+
4
+ java_import 'java.io.RandomAccessFile'
5
+ java_import 'java.net.InetSocketAddress'
6
+ java_import 'java.util.concurrent.Executors'
7
+ java_import 'org.jboss.netty.bootstrap.ServerBootstrap'
8
+ java_import 'org.jboss.netty.buffer.ChannelBuffer'
9
+ java_import 'org.jboss.netty.buffer.ChannelBufferInputStream'
10
+ java_import 'org.jboss.netty.buffer.ChannelBuffers'
11
+ java_import 'org.jboss.netty.handler.codec.http.DefaultHttpResponse'
12
+ java_import 'org.jboss.netty.handler.codec.http.HttpContentDecompressor'
13
+ java_import 'org.jboss.netty.handler.codec.http.HttpHeaders'
14
+ java_import 'org.jboss.netty.handler.codec.http.HttpChunk'
15
+ java_import 'org.jboss.netty.handler.codec.http.HttpRequest'
16
+ java_import 'org.jboss.netty.handler.codec.http.HttpRequestDecoder'
17
+ java_import 'org.jboss.netty.handler.codec.http.HttpResponseEncoder'
18
+ java_import 'org.jboss.netty.handler.codec.http.HttpResponseStatus'
19
+ java_import 'org.jboss.netty.handler.codec.http.HttpVersion'
20
+ java_import 'org.jboss.netty.handler.codec.http.QueryStringDecoder'
21
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdyFrameDecoder'
22
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdyFrameEncoder'
23
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdySessionHandler'
24
+ java_import 'org.jboss.netty.handler.execution.ExecutionHandler'
25
+ java_import 'org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor'
26
+ java_import 'org.jboss.netty.handler.logging.LoggingHandler'
27
+ java_import 'org.jboss.netty.handler.stream.ChunkedStream'
28
+ java_import 'org.jboss.netty.handler.stream.ChunkedWriteHandler'
29
+ java_import 'org.jboss.netty.channel.DefaultFileRegion'
30
+ java_import 'org.jboss.netty.channel.ChannelFutureListener'
31
+ java_import 'org.jboss.netty.channel.ChannelPipelineFactory'
32
+ java_import 'org.jboss.netty.channel.Channels'
33
+ java_import 'org.jboss.netty.channel.SimpleChannelUpstreamHandler'
34
+ java_import 'org.jboss.netty.channel.ChannelUpstreamHandler'
35
+ java_import 'org.jboss.netty.channel.ChannelDownstreamHandler'
36
+ java_import 'org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory'
37
+ java_import 'org.jboss.netty.logging.InternalLogLevel'
38
+ java_import 'org.jboss.netty.util.CharsetUtil'
39
+
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ module Thick
2
+
3
+ class TimeProbe
4
+
5
+ include Thick::Java::ChannelUpstreamHandler
6
+ include Thick::Java::ChannelDownstreamHandler
7
+
8
+ def self.setup
9
+ @total = 0
10
+ @count = 0
11
+ end
12
+
13
+ def initialize
14
+ @time = Time.now.to_f
15
+ end
16
+
17
+ TimeProbe.setup
18
+
19
+ def self.time(time)
20
+ @total += time
21
+ @count += 1
22
+ if @count % 100 == 0
23
+ puts "AVG: #{@total / @count}"
24
+ setup
25
+ end
26
+ end
27
+
28
+ def handleUpstream(context, event)
29
+ context.send_upstream(event)
30
+ end
31
+
32
+ def handleDownstream(context, event)
33
+ if event.respond_to?(:getState)
34
+ stop = Time.now.to_f - @time
35
+ self.class.time(stop)
36
+ end
37
+ context.send_downstream(event)
38
+ end
39
+
40
+ end
41
+
42
+ class PipelineFactory
43
+
44
+ include Thick::Java::ChannelPipelineFactory
45
+
46
+ def initialize(application)
47
+ @application = application
48
+ @executor = Thick::Java::ExecutionHandler.new(Thick::Java::OrderedMemoryAwareThreadPoolExecutor.new(20, 0, 0))
49
+ end
50
+
51
+ def getPipeline
52
+ pipeline = Thick::Java::Channels.pipeline
53
+
54
+ pipeline.add_last('time_probe', TimeProbe.new)
55
+
56
+ pipeline.add_last('http_decoder', Thick::Java::HttpRequestDecoder.new)
57
+ pipeline.add_last('http_encoder', Thick::Java::HttpResponseEncoder.new)
58
+
59
+ pipeline.add_last('decompressor', Thick::Java::HttpContentDecompressor.new)
60
+
61
+ pipeline.add_last('executor', @executor)
62
+
63
+ pipeline.add_last('handler', ServerHandler.new(@application))
64
+
65
+ pipeline
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,31 @@
1
+ module Thick
2
+
3
+ def self.start(application, options)
4
+ @server = Server.new(application)
5
+ @server.start(:address => options[:address], :port => options[:port])
6
+ end
7
+
8
+ class Server
9
+
10
+ def initialize(application)
11
+ @factory = Thick::Java::NioServerSocketChannelFactory.new(Thick::Java::Executors.new_cached_thread_pool, Thick::Java::Executors.new_cached_thread_pool)
12
+ @bootstrap = Thick::Java::ServerBootstrap.new(@factory)
13
+ @bootstrap.pipeline_factory = PipelineFactory.new(application)
14
+ end
15
+
16
+ def start(options = {})
17
+ @channel = @bootstrap.bind(Thick::Java::InetSocketAddress.new(options[:address], options[:port]))
18
+ puts 'Server started'
19
+ loop do
20
+ # ToDo: Is this hack needed?
21
+ sleep(100)
22
+ end
23
+ end
24
+
25
+ def stop
26
+ @channel.close
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,138 @@
1
+ require 'stringio'
2
+
3
+ module Thick
4
+
5
+ class ServerHandler < Thick::Java::SimpleChannelUpstreamHandler
6
+
7
+ def initialize(application)
8
+ super()
9
+ @application = application
10
+ end
11
+
12
+ def channelConnected(context, event)
13
+ end
14
+
15
+ def messageReceived(context, event)
16
+ request = event.message
17
+ case request
18
+ when Thick::Java::HttpRequest
19
+ on_request(context, request)
20
+ when Thick::Java::HttpChunk
21
+ on_chunk(context, request)
22
+ end
23
+ end
24
+
25
+ def serve_file(context, file, from = 0, to = nil)
26
+ raf = Thick::Java::RandomAccessFile.new(file, "r")
27
+ to ||= File.size(file)
28
+ region = Thick::Java::DefaultFileRegion.new(raf.channel, from, to)
29
+ context.channel.write(region)
30
+ end
31
+
32
+ def on_request(context, request)
33
+
34
+ # Serve file if exists in public directory
35
+ file = File.join('public', request.uri)
36
+ return serve_file(context, file).addListener(Thick::Java::ChannelFutureListener::CLOSE) if !request.uri.index('..') && File.exists?(file) && !File.directory?(file)
37
+
38
+ query_string = request.uri.split('?', 2)
39
+ @env = {
40
+ # Rack specific
41
+ 'rack.version' => [1,1],
42
+ 'rack.url_scheme' => 'http', # ToDo: support https?
43
+ 'rack.input' => Buffer.new,
44
+ 'rack.errors' => $stdout, # ToDo: Sure about that?
45
+ 'rack.multithread' => true,
46
+ 'rack.multiprocess' => false,
47
+ 'rack.run_once' => false,
48
+ # HTTP specific
49
+ 'REQUEST_METHOD' => request.get_method.name,
50
+ 'SCRIPT_NAME' => '',
51
+ 'PATH_INFO' => query_string[0],
52
+ 'QUERY_STRING' => if query_string[1] && query_string[1] != '' then "?#{query_string[1]}" else '' end,
53
+ 'SERVER_NAME' => 'localhost', # ToDo: Be more precise!
54
+ 'SERVER_PORT' => '8080', # ToDo: Be more precise!
55
+ # Thick specific
56
+ 'thick.async' => AsyncResponse.new(context)
57
+ }
58
+
59
+ # Get content length if available
60
+ length = Thick::Java::HttpHeaders.get_content_length(request)
61
+ @env['CONTENT_LENGTH'] = length if length > 0
62
+
63
+ # Extract headers and normalize it's names
64
+ request.header_names.each do |name|
65
+ rack_name = name.gsub('-', '_').upcase
66
+ rack_name = "HTTP_#{rack_name}" unless %w(CONTENT_TYPE CONTENT_LENGTH).include?(rack_name)
67
+ @env[rack_name] = request.get_header(name)
68
+ end
69
+
70
+ # Write request content into prepared IO
71
+ @env['rack.input'].write(request.content)
72
+
73
+ # No chunks expected, handle request
74
+ handle_request(context) unless request.chunked?
75
+ end
76
+
77
+ def on_chunk(context, chunk)
78
+ # Write request content into prepared IO
79
+ @env['rack.input'].write(chunk.content)
80
+
81
+ # No more chunks expected, handle request
82
+ handle_request(context) if chunk.last?
83
+ end
84
+
85
+ def handle_request(context)
86
+ response = @application.call(@env)
87
+
88
+ # Let the application make the response as it wants
89
+ if @env['thick.async'].custom?
90
+ @env['thick.async'].ready!
91
+ return
92
+ end
93
+
94
+ # Unpack response
95
+ status, headers, content = response
96
+
97
+ # Create response and set status as requested by application
98
+ @response = Thick::Java::DefaultHttpResponse.new(Thick::Java::HttpVersion::HTTP_1_1, Thick::Java::HttpResponseStatus.value_of(status.to_i))
99
+
100
+ # Set headers as requested by application
101
+ headers.each do |name, value|
102
+ Thick::Java::HttpHeaders.set_header(@response, name, value)
103
+ end
104
+
105
+ context.channel.write(@response)
106
+
107
+ # Set content as requested by application
108
+ case
109
+ when content.respond_to?(:to_path)
110
+ serve_file(context, content.to_path)
111
+ else
112
+ content.each do |chunk|
113
+ context.channel.write(Thick::Java::ChannelBuffers.copiedBuffer(chunk, Thick::Java::CharsetUtil::UTF_8))
114
+ end
115
+ end
116
+
117
+ # When content is closable, close it
118
+ content.close if content.respond_to?(:close)
119
+
120
+ # Trigger async handler and marker if async response requested, close connection otherwise
121
+ if @env['thick.async'].async?
122
+ @env['thick.async'].ready!
123
+ else
124
+ @env['thick.async'].close
125
+ end
126
+ end
127
+
128
+ def channelClosed(context, event)
129
+ end
130
+
131
+ def exceptionCaught(context, e)
132
+ puts e.message if e.respond_to?(:message)
133
+ puts e.backtrace if e.respond_to?(:backtrace)
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,3 @@
1
+ module Thick
2
+ VERSION = '0.0.4' unless const_defined?(:VERSION)
3
+ end
data/lib/thick.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'rack'
2
+
3
+ require 'java'
4
+
5
+ require File.expand_path('../jars/netty-3.4.0.Alpha2.jar', __FILE__)
6
+
7
+ require 'thick/java'
8
+ require 'thick/version'
9
+ require 'thick/buffer'
10
+ require 'thick/async_response'
11
+ require 'thick/server_handler'
12
+ require 'thick/pipeline_factory'
13
+ require 'thick/server'
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thick
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.4
6
+ platform: java
7
+ authors:
8
+ - Marek Jelen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-03-29 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.4.1
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description: Very lightweight web server for JRuby based on Netty library.
27
+ email:
28
+ - marek@jelen.biz
29
+ executables:
30
+ - thick
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - bin/thick
37
+ - lib/thick.rb
38
+ - lib/jars/netty-3.4.0.Alpha2.jar
39
+ - lib/thick/async_response.rb
40
+ - lib/thick/buffer.rb
41
+ - lib/thick/java.rb
42
+ - lib/thick/pipeline_factory.rb
43
+ - lib/thick/server.rb
44
+ - lib/thick/server_handler.rb
45
+ - lib/thick/version.rb
46
+ - LICENSE
47
+ - README.md
48
+ - ROADMAP.md
49
+ - CHANGELOG.md
50
+ homepage: http://github.com/marekjelen/thick
51
+ licenses: []
52
+
53
+ post_install_message:
54
+ rdoc_options: []
55
+
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.15
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Very lightweight web server for JRuby
77
+ test_files: []
78
+