adelnor 0.0.6 → 0.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd3a506fee52615e09d8aed4c85bea4a9dfdc58567a373b40b5960d903dc621f
4
- data.tar.gz: b69d40e33b17d24932f6feee996116c785660e9519b9062a95de258bca85d0b7
3
+ metadata.gz: 9b8a98417998eb15f466baf73a5943c808ad1efe160bc87b268464b1b9096bdc
4
+ data.tar.gz: 8edef2ff4470feefa0273c29b03c03da7245ee48de5df6fad9121798bf10d568
5
5
  SHA512:
6
- metadata.gz: f2f5ea6b2e64cdb6f9f5447a7590d73fa5bcb1b251b3e438b56bd9bc44a8112a9dc95bbf8492a871f29c61c4dc15b6604673517a3d06035d1234fbcedbfec8e0
7
- data.tar.gz: 99c4805e7b08d6948021d03fdee3c5c9dd311485e2d2bfde2c773241cfb900ce548fe8a5297044ecd9fbc43a3abc06faca0862fc626af620278740a16874a8ec
6
+ metadata.gz: 1b5dc9aa3a65158ee1b1d7cc988136b852b7125f646e5f90a1b6154a37c6d2d989f6b1523a391e7c97b76a8934b7dd9b84944e586d57a7236f465f05cd152184
7
+ data.tar.gz: dcd17f72dfe0279174f4d4e10c48c7cdac44e0ccfa073241715be14ed7fa075ef85cccb1fa7ec3df84fcf968a93714928887f8a4b8dfc69fdf87d07c056efbcc
data/Gemfile CHANGED
@@ -2,5 +2,6 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ gem 'async'
5
6
  gem 'rack'
6
7
  gem 'rubocop', require: false
data/Gemfile.lock CHANGED
@@ -2,6 +2,17 @@ GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
4
  ast (2.4.2)
5
+ async (2.6.4)
6
+ console (~> 1.10)
7
+ fiber-annotation
8
+ io-event (~> 1.1)
9
+ timers (~> 4.1)
10
+ console (1.23.2)
11
+ fiber-annotation
12
+ fiber-local
13
+ fiber-annotation (0.2.0)
14
+ fiber-local (1.0.0)
15
+ io-event (1.3.2)
5
16
  parallel (1.22.1)
6
17
  parser (3.1.2.0)
7
18
  ast (~> 2.4.1)
@@ -21,12 +32,15 @@ GEM
21
32
  rubocop-ast (1.18.0)
22
33
  parser (>= 3.1.1.0)
23
34
  ruby-progressbar (1.11.0)
35
+ timers (4.3.5)
24
36
  unicode-display_width (2.1.0)
25
37
 
26
38
  PLATFORMS
27
39
  aarch64-linux
40
+ x86_64-linux
28
41
 
29
42
  DEPENDENCIES
43
+ async
30
44
  rack
31
45
  rubocop
32
46
 
data/README.md CHANGED
@@ -7,12 +7,13 @@
7
7
  ![Build](https://github.com/leandronsp/adelnor/actions/workflows/build.yml/badge.svg)
8
8
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
9
9
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
10
- ``` .___ .__
11
- _____ __| _/____ | | ____ ___________
12
- \__ \ / __ |/ __ \| | / \ / _ \_ __ \
13
- / __ \_/ /_/ \ ___/| |_| | ( <_> ) | \/
14
- (____ /\____ |\___ >____/___| /\____/|__|
15
- \/ \/ \/ \/
10
+ ```
11
+ ___ _______ _______ __ .__ __. ______ .______
12
+ / \ | \ | ____|| | | \ | | / __ \ | _ \
13
+ / ^ \ | .--. || |__ | | | \| | | | | | | |_) |
14
+ / /_\ \ | | | || __| | | | . ` | | | | | | /
15
+ / _____ \ | '--' || |____ | `----.| |\ | | `--' | | |\ \----.
16
+ /__/ \__\ |_______/ |_______||_______||__| \__| \______/ | _| `._____|
16
17
  ```
17
18
 
18
19
  [adelnor](https://rubygems.org/gems/adelnor) is a dead simple, yet Rack-compatible, HTTP server written in Ruby.
@@ -56,4 +57,25 @@ $ make bundle.install
56
57
  $ make sample.server
57
58
  ```
58
59
 
60
+ ## Handling concurrency
61
+
62
+ Currently Adelnor allows to run the server using different concurrency strategies, one at a time.
63
+
64
+ ```ruby
65
+ require './lib/adelnor/server'
66
+
67
+ app = -> (env) do
68
+ [200, { 'Content-Type' => 'text/html' }, 'Hello world!']
69
+ end
70
+
71
+ # Threaded mode
72
+ Adelnor::Server.run app, 3000, thread_pool: 5
73
+
74
+ # Clusterd mode (forking)
75
+ Adelnor::Server.run app, 3000, workers: 2
76
+
77
+ # Async mode
78
+ Adelnor::Server.run app, 3000, async: true
79
+ ```
80
+
59
81
  Open `http://localhost:3000`
data/adelnor.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'adelnor'
5
- spec.version = '0.0.6'
5
+ spec.version = '0.0.8'
6
6
  spec.summary = 'Adelnor HTTP server'
7
7
  spec.description = 'A dead simple, yet Rack-compatible, HTTP server written in Ruby'
8
8
  spec.authors = ['Leandro Proença']
@@ -11,7 +11,8 @@ Gem::Specification.new do |spec|
11
11
  spec.homepage = 'https://github.com/leandronsp/adelnor'
12
12
  spec.license = 'MIT'
13
13
 
14
- spec.add_runtime_dependency 'rack'
14
+ spec.add_runtime_dependency 'async', '~> 2.6'
15
+ spec.add_runtime_dependency 'rack', '~> 2.2'
15
16
 
16
17
  spec.required_ruby_version = '~> 3.0'
17
18
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_server'
4
+ require 'async'
5
+ require 'async/scheduler'
6
+
7
+ module Adelnor
8
+ class AsyncServer < BaseServer
9
+ def initialize(rack_app, port, options = {})
10
+ super(rack_app, port, options)
11
+
12
+ scheduler = Async::Scheduler.new
13
+ Fiber.set_scheduler(scheduler)
14
+ end
15
+
16
+ def run
17
+ Async do |task|
18
+ loop do
19
+ client, = @socket.accept
20
+
21
+ task.async do
22
+ handle(client)
23
+ client.close
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'rack'
5
+ require 'stringio'
6
+
7
+ require_relative './request'
8
+ require_relative './response'
9
+
10
+ module Adelnor
11
+ class BaseServer
12
+ def initialize(rack_app, port, options = {})
13
+ @rack_app = rack_app
14
+ @port = port
15
+ @options = options
16
+ @socket = Socket.new(:INET, :STREAM)
17
+ addr = Socket.sockaddr_in(@port, '0.0.0.0')
18
+
19
+ @socket.bind(addr)
20
+ @socket.listen(2)
21
+
22
+ puts welcome_message
23
+ end
24
+
25
+ def self.run(*args)
26
+ new(*args).run
27
+ end
28
+
29
+ def rack_data(request)
30
+ {
31
+ 'REQUEST_METHOD' => request.request_method,
32
+ 'PATH_INFO' => request.path_info,
33
+ 'QUERY_STRING' => request.query_string,
34
+ 'SERVER_PORT' => @port,
35
+ 'SERVER_NAME' => request.headers['Host'],
36
+ 'CONTENT_LENGTH' => request.content_length,
37
+ 'HTTP_COOKIE' => request.headers['Cookie'],
38
+ 'rack.input' => StringIO.new(request.body)
39
+ }.merge(request.headers)
40
+ end
41
+
42
+ def run
43
+ loop do
44
+ client, = @socket.accept
45
+
46
+ handle(client)
47
+ client.close
48
+ end
49
+ end
50
+
51
+ def handle(client)
52
+ read_request_message(client)
53
+ .then { |message| Request.build(message) }
54
+ .tap { |request| request.parse_body!(client) }
55
+ .then { |request| rack_data(request) }
56
+ .then { |data| @rack_app.call(data) }
57
+ .then { |result| Response.build(*result) }
58
+ .then { |response| client.puts(response) }
59
+ end
60
+
61
+ def read_request_message(client)
62
+ message = ''
63
+
64
+ if (line = client.gets)
65
+ message += line
66
+ end
67
+
68
+ puts "\n[#{Time.now}] #{message}"
69
+
70
+ while (line = client.gets)
71
+ break if line == "\r\n"
72
+
73
+ message += line
74
+ end
75
+
76
+ message
77
+ end
78
+
79
+ def welcome_message
80
+ thread_pool_message = "Running with thread pool of #{@options[:thread_pool]} threads" if @options[:thread_pool]
81
+
82
+ <<~WELCOME
83
+ |\---/|
84
+ | o_o |
85
+ \_^_/
86
+
87
+ adelnor HTTP server
88
+
89
+ ------------------------------------------------
90
+
91
+ Adelnor is running at http://0.0.0.0:#{@port}
92
+ Listening to HTTP connections
93
+
94
+ #{thread_pool_message}
95
+ WELCOME
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_server'
4
+
5
+ module Adelnor
6
+ class ClusteredServer < BaseServer
7
+ def initialize(rack_app, port, options = {})
8
+ super(rack_app, port, options)
9
+
10
+ @workers = options[:workers]
11
+ end
12
+
13
+ def run
14
+ @workers.times do
15
+ fork { handle_worker }
16
+ end
17
+
18
+ Process.waitall
19
+ end
20
+
21
+ def handle_worker
22
+ pid = Process.pid
23
+ puts "[#{pid}] Worker started"
24
+
25
+ loop do
26
+ client, = @socket.accept
27
+
28
+ handle(client)
29
+ client.close
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
4
+ require_relative './base_server'
5
+
6
+ module Adelnor
7
+ class RactorServer < BaseServer
8
+ def initialize(rack_app, port, options = {})
9
+ @ractors = options[:ractors]
10
+
11
+ @rack_app = rack_app
12
+ @port = port
13
+ @options = options
14
+
15
+ @queue = Ractor.new do
16
+ loop do
17
+ Ractor.yield(Ractor.receive, move: true)
18
+ end
19
+ end
20
+
21
+ @listener = Ractor.new(@queue, @port) do |queue, port|
22
+ socket = Socket.new(:INET, :STREAM)
23
+ addr = Socket.sockaddr_in(port, '0.0.0.0')
24
+
25
+ socket.bind(addr)
26
+ socket.listen(2)
27
+
28
+ loop do
29
+ client, = socket.accept
30
+
31
+ queue.send(client, move: true)
32
+ end
33
+ end
34
+
35
+ puts welcome_message
36
+ end
37
+
38
+ def run
39
+ ractors = @ractors.times.map do
40
+ Ractor.new(@queue, @rack_app, @port) do |queue, rack_app, port|
41
+ rid = Ractor.current.object_id
42
+ puts "[#{rid}] Ractor started"
43
+
44
+ loop do
45
+ client = queue.take
46
+ message = ''
47
+
48
+ if (line = client.gets)
49
+ message += line
50
+ end
51
+
52
+ puts "\n[#{Time.now}] #{message}"
53
+
54
+ while (line = client.gets)
55
+ break if line == "\r\n"
56
+
57
+ message += line
58
+ end
59
+
60
+ rack_data = lambda do |request|
61
+ {
62
+ 'REQUEST_METHOD' => request.request_method,
63
+ 'PATH_INFO' => request.path_info,
64
+ 'QUERY_STRING' => request.query_string,
65
+ 'SERVER_PORT' => port,
66
+ 'SERVER_NAME' => request.headers['Host'],
67
+ 'CONTENT_LENGTH' => request.content_length,
68
+ 'HTTP_COOKIE' => request.headers['Cookie'],
69
+ 'rack.input' => StringIO.new(request.body)
70
+ }
71
+ end
72
+
73
+ Request.build(message)
74
+ .tap { |request| request.parse_body!(client) }
75
+ .then { |request| rack_data.call(request).merge(request.headers) }
76
+ .then { |data| rack_app.call(data) }
77
+ .then { |result| Response.build(*result) }
78
+ .then { |response| client.puts(response) }
79
+
80
+ client.close
81
+ end
82
+ end
83
+ end
84
+
85
+ loop do
86
+ Ractor.select(@listener, *ractors)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ # rubocop:enable all
@@ -21,6 +21,8 @@ module Adelnor
21
21
  lines = @message.split(/\r\n/)
22
22
  headline = lines.shift
23
23
 
24
+ return self unless headline
25
+
24
26
  parse_headline!(headline)
25
27
  parse_headers!(lines)
26
28
 
@@ -19,6 +19,6 @@ module Adelnor
19
19
  private
20
20
 
21
21
  def headers_as_string = @headers.reduce('', &method(:acc_header_string))
22
- def acc_header_string(acc, (key, value)) = acc += "#{key}: #{value}\r\n"
22
+ def acc_header_string(acc, (key, value)) = acc + "#{key}: #{value}\r\n"
23
23
  end
24
24
  end
@@ -1,92 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'socket'
4
- require 'rack'
5
- require 'stringio'
6
-
7
- require_relative './request'
8
- require_relative './response'
3
+ require_relative './base_server'
4
+ require_relative './threaded_server'
5
+ require_relative './clustered_server'
6
+ require_relative './ractor_server'
7
+ require_relative './async_server'
9
8
 
10
9
  module Adelnor
11
10
  class Server
12
- def initialize(rack_app, port)
11
+ def initialize(rack_app, port, options = {})
13
12
  @rack_app = rack_app
14
13
  @port = port
15
- @socket = Socket.new(:INET, :STREAM)
16
- addr = Socket.sockaddr_in(@port, '0.0.0.0')
17
-
18
- @socket.bind(addr)
19
- @socket.listen(2)
20
-
21
- puts welcome_message
14
+ @options = options
22
15
  end
23
16
 
24
17
  def self.run(*args)
25
18
  new(*args).run
26
19
  end
27
20
 
28
- def rack_data(request)
29
- {
30
- 'REQUEST_METHOD' => request.request_method,
31
- 'PATH_INFO' => request.path_info,
32
- 'QUERY_STRING' => request.query_string,
33
- 'SERVER_PORT' => @port,
34
- 'SERVER_NAME' => request.headers['Host'],
35
- 'CONTENT_LENGTH' => request.content_length,
36
- 'HTTP_COOKIE' => request.headers['Cookie'],
37
- 'rack.input' => StringIO.new(request.body)
38
- }.merge(request.headers)
39
- end
40
-
41
21
  def run
42
- loop do
43
- client, = @socket.accept
44
-
45
- handle(client)
46
-
47
- client.close
48
- end
49
- end
50
-
51
- def handle(client)
52
- read_request_message(client)
53
- .then { |message| Request.build(message) }
54
- .tap { |request| request.parse_body!(client) }
55
- .then { |request| rack_data(request) }
56
- .then { |data| @rack_app.call(data) }
57
- .then { |result| Response.build(*result) }
58
- .then { |response| client.puts(response) }
59
- end
60
-
61
- def read_request_message(client)
62
- message = ''
63
-
64
- message += client.gets
65
- puts
66
- puts "[#{Time.now}] #{message}"
67
-
68
- while (line = client.gets)
69
- break if line == "\r\n"
70
-
71
- message += line
72
- end
73
-
74
- message
75
- end
76
-
77
- def welcome_message
78
- <<~WELCOME
79
- |\---/|
80
- | o_o |
81
- \_^_/
82
-
83
- adelnor HTTP server
84
-
85
- ------------------------------------------------
86
-
87
- Adelnor is running at http://0.0.0.0:#{@port}
88
- Listening to HTTP connections
89
- WELCOME
22
+ handler_klass = if @options[:thread_pool]
23
+ ThreadedServer
24
+ elsif @options[:workers]
25
+ ClusteredServer
26
+ # elsif @options[:ractors]
27
+ # RactorServer
28
+ elsif @options[:async]
29
+ AsyncServer
30
+ else
31
+ BaseServer
32
+ end
33
+
34
+ handler_klass.run(@rack_app, @port, @options)
90
35
  end
91
36
  end
92
37
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_server'
4
+
5
+ module Adelnor
6
+ class ThreadedServer < BaseServer
7
+ def initialize(rack_app, port, options = {})
8
+ super(rack_app, port, options)
9
+
10
+ @thread_queue = Queue.new
11
+ @pool_size = options[:thread_pool]
12
+ end
13
+
14
+ def run
15
+ @pool_size.times do
16
+ Thread.new { handle_thread }
17
+ end
18
+
19
+ loop do
20
+ client, = @socket.accept
21
+
22
+ @thread_queue.push(client)
23
+ end
24
+ end
25
+
26
+ def handle_thread
27
+ tid = Thread.current.object_id
28
+
29
+ puts "[#{tid}] Thread started"
30
+ loop do
31
+ client = @thread_queue.pop
32
+
33
+ handle(client)
34
+ client.close
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Adelnor
4
- VERSION = '0.0.6'
4
+ VERSION = '0.0.8'
5
5
  end
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adelnor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Proença
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-22 00:00:00.000000000 Z
11
+ date: 2023-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rack
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - ">="
31
+ - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '0'
33
+ version: '2.2'
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
- - - ">="
38
+ - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '0'
40
+ version: '2.2'
27
41
  description: A dead simple, yet Rack-compatible, HTTP server written in Ruby
28
42
  email: leandronsp@gmail.com
29
43
  executables: []
@@ -35,15 +49,20 @@ files:
35
49
  - README.md
36
50
  - adelnor.gemspec
37
51
  - lib/adelnor.rb
52
+ - lib/adelnor/async_server.rb
53
+ - lib/adelnor/base_server.rb
54
+ - lib/adelnor/clustered_server.rb
55
+ - lib/adelnor/ractor_server.rb
38
56
  - lib/adelnor/request.rb
39
57
  - lib/adelnor/response.rb
40
58
  - lib/adelnor/server.rb
59
+ - lib/adelnor/threaded_server.rb
41
60
  - lib/adelnor/version.rb
42
61
  homepage: https://github.com/leandronsp/adelnor
43
62
  licenses:
44
63
  - MIT
45
64
  metadata: {}
46
- post_install_message:
65
+ post_install_message:
47
66
  rdoc_options: []
48
67
  require_paths:
49
68
  - lib
@@ -58,8 +77,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
77
  - !ruby/object:Gem::Version
59
78
  version: '0'
60
79
  requirements: []
61
- rubygems_version: 3.3.3
62
- signing_key:
80
+ rubygems_version: 3.4.10
81
+ signing_key:
63
82
  specification_version: 4
64
83
  summary: Adelnor HTTP server
65
84
  test_files: []