adelnor 0.0.7 → 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: 5bbe7f5e96936b78536738d717ea5babeb525aec4ecc202774e6564095d21663
4
- data.tar.gz: 8da6cf7523a6d8f5d520fe6eb1c8e02320b53e79f631b0e36d0401a29f093f47
3
+ metadata.gz: 9b8a98417998eb15f466baf73a5943c808ad1efe160bc87b268464b1b9096bdc
4
+ data.tar.gz: 8edef2ff4470feefa0273c29b03c03da7245ee48de5df6fad9121798bf10d568
5
5
  SHA512:
6
- metadata.gz: b46502cfb371089998df7cf3d98221224df23ebb77cfd6f368d9d3b21df8ef8244a8cde14207c8b511146011fcd6766498483288ade45f263fdb00b4c7ed3927
7
- data.tar.gz: d7fdfbe381e3c56c4c6bfdc7109afdd10ee9bd05c7d95d5da012fe671f3b547763a3bb916e519c2228038a1a6be7c473236c5bb623662e0ff7b8f7672209349b
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
@@ -57,7 +57,9 @@ $ make bundle.install
57
57
  $ make sample.server
58
58
  ```
59
59
 
60
- ## Using Thread Pool
60
+ ## Handling concurrency
61
+
62
+ Currently Adelnor allows to run the server using different concurrency strategies, one at a time.
61
63
 
62
64
  ```ruby
63
65
  require './lib/adelnor/server'
@@ -66,7 +68,14 @@ app = -> (env) do
66
68
  [200, { 'Content-Type' => 'text/html' }, 'Hello world!']
67
69
  end
68
70
 
71
+ # Threaded mode
69
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
70
79
  ```
71
80
 
72
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.7'
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
@@ -1,11 +1,10 @@
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
@@ -13,117 +12,26 @@ module Adelnor
13
12
  @rack_app = rack_app
14
13
  @port = port
15
14
  @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
- @thread_queue = Queue.new if @options[:thread_pool]
23
- puts welcome_message
24
15
  end
25
16
 
26
17
  def self.run(*args)
27
18
  new(*args).run
28
19
  end
29
20
 
30
- def rack_data(request)
31
- {
32
- 'REQUEST_METHOD' => request.request_method,
33
- 'PATH_INFO' => request.path_info,
34
- 'QUERY_STRING' => request.query_string,
35
- 'SERVER_PORT' => @port,
36
- 'SERVER_NAME' => request.headers['Host'],
37
- 'CONTENT_LENGTH' => request.content_length,
38
- 'HTTP_COOKIE' => request.headers['Cookie'],
39
- 'rack.input' => StringIO.new(request.body)
40
- }.merge(request.headers)
41
- end
42
-
43
21
  def run
44
- if (pool_size = @options[:thread_pool])
45
- return run_with_thread_pool(pool_size)
46
- end
47
-
48
- loop do
49
- client, = @socket.accept
50
-
51
- handle(client)
52
- client.close
53
- end
54
- end
55
-
56
- def run_with_thread_pool(pool_size)
57
- pool_size.times do
58
- handle_request_in_thread
59
- end
60
-
61
- loop do
62
- client, = @socket.accept
63
-
64
- @thread_queue.push(client)
65
- end
66
- end
67
-
68
- def handle_request_in_thread
69
- Thread.new do
70
- tid = Thread.current.object_id
71
-
72
- puts "[#{tid}] Thread started"
73
- loop do
74
- client = @thread_queue.pop
75
-
76
- handle(client)
77
- client.close
78
- end
79
- end
80
- end
81
-
82
- def handle(client)
83
- read_request_message(client)
84
- .then { |message| Request.build(message) }
85
- .tap { |request| request.parse_body!(client) }
86
- .then { |request| rack_data(request) }
87
- .then { |data| @rack_app.call(data) }
88
- .then { |result| Response.build(*result) }
89
- .then { |response| client.puts(response) }
90
- end
91
-
92
- def read_request_message(client)
93
- message = ''
94
-
95
- if (line = client.gets)
96
- message += line
97
- end
98
-
99
- puts "\n[#{Time.now}] #{message}"
100
-
101
- while (line = client.gets)
102
- break if line == "\r\n"
103
-
104
- message += line
105
- end
106
-
107
- message
108
- end
109
-
110
- def welcome_message
111
- thread_pool_message = "Running with thread pool of #{@options[:thread_pool]} threads" if @options[:thread_pool]
112
-
113
- <<~WELCOME
114
- |\---/|
115
- | o_o |
116
- \_^_/
117
-
118
- adelnor HTTP server
119
-
120
- ------------------------------------------------
121
-
122
- Adelnor is running at http://0.0.0.0:#{@port}
123
- Listening to HTTP connections
124
-
125
- #{thread_pool_message}
126
- 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)
127
35
  end
128
36
  end
129
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.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Proença
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-05 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,9 +49,14 @@ 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: