polyphony 0.44.0 → 0.45.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile.lock +9 -11
  5. data/Rakefile +1 -1
  6. data/TODO.md +12 -7
  7. data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
  8. data/docs/api-reference/thread.md +1 -1
  9. data/docs/getting-started/overview.md +14 -14
  10. data/docs/getting-started/tutorial.md +1 -1
  11. data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
  12. data/examples/io/xx-pry.rb +18 -0
  13. data/examples/io/xx-rack_server.rb +71 -0
  14. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
  15. data/ext/polyphony/backend.h +41 -0
  16. data/ext/polyphony/event.c +3 -3
  17. data/ext/polyphony/extconf.rb +1 -1
  18. data/ext/polyphony/{libev_agent.c → libev_backend.c} +175 -175
  19. data/ext/polyphony/polyphony.c +1 -1
  20. data/ext/polyphony/polyphony.h +4 -4
  21. data/ext/polyphony/polyphony_ext.c +2 -2
  22. data/ext/polyphony/queue.c +2 -2
  23. data/ext/polyphony/thread.c +21 -21
  24. data/lib/polyphony.rb +13 -12
  25. data/lib/polyphony/adapters/irb.rb +2 -17
  26. data/lib/polyphony/adapters/mysql2.rb +1 -1
  27. data/lib/polyphony/adapters/postgres.rb +5 -5
  28. data/lib/polyphony/adapters/process.rb +2 -2
  29. data/lib/polyphony/adapters/readline.rb +17 -0
  30. data/lib/polyphony/adapters/sequel.rb +1 -1
  31. data/lib/polyphony/core/global_api.rb +11 -6
  32. data/lib/polyphony/core/resource_pool.rb +2 -2
  33. data/lib/polyphony/core/sync.rb +38 -2
  34. data/lib/polyphony/core/throttler.rb +1 -1
  35. data/lib/polyphony/extensions/core.rb +31 -20
  36. data/lib/polyphony/extensions/fiber.rb +1 -1
  37. data/lib/polyphony/extensions/io.rb +7 -8
  38. data/lib/polyphony/extensions/openssl.rb +6 -6
  39. data/lib/polyphony/extensions/socket.rb +4 -14
  40. data/lib/polyphony/extensions/thread.rb +6 -5
  41. data/lib/polyphony/version.rb +1 -1
  42. data/polyphony.gemspec +4 -3
  43. data/test/helper.rb +1 -1
  44. data/test/{test_agent.rb → test_backend.rb} +22 -22
  45. data/test/test_fiber.rb +4 -4
  46. data/test/test_io.rb +1 -1
  47. data/test/test_kernel.rb +5 -0
  48. data/test/test_signal.rb +3 -3
  49. data/test/test_sync.rb +52 -0
  50. metadata +40 -30
  51. data/.gitbook.yaml +0 -4
  52. data/ext/polyphony/agent.h +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04c1c1221d46b1b9169aa04c5896c8d68c20fa11d64554e76b7452710423f142
4
- data.tar.gz: 519528e0a3dfd323620869d4a7e43e4769cf3c1b5f0ea405cf341efc6fd534f6
3
+ metadata.gz: f9c8ab74213c6cc5e3852f73ee027ee496f2b04c6844e09856c9771ea5e7839a
4
+ data.tar.gz: 83d7c533024b6d6d633b9e18abc392911adfc07f728af826bd84cce35cafb20c
5
5
  SHA512:
6
- metadata.gz: de8ac460dd05b051057ffab1c155c6b2d675cc623b98549333ba1defbc99f4efb738ff4a34a9f8c14bd3cb9ab0cab5356aa34a2308daa73273a6ef8719565bc4
7
- data.tar.gz: c26b254559e484774574555dc895cf072bda8061b92fed26d8e61e7172523fe0faf2750cd2f924db3a25b59583b673ebe5f14749fd6f1279ca2e7e7b7745490b
6
+ metadata.gz: c42e49ebcd6fb10b384438cb7688801a387963bc2a6fb2c1d3de6d022a7731a7d88db138a34b364b35c9d71830b975a69cb1f14fb277166a9db1802e24e82dec
7
+ data.tar.gz: 6c578eacded00dd7a77a35124c69ebc966e4f9aa42264913df82af147f43955a2ecd86087630c085fcd4d6e964aa705202c5aa24bb4032648d3ba278d60d4f63
@@ -81,6 +81,7 @@ Lint/SuppressedException:
81
81
  - examples/**/*.rb
82
82
 
83
83
  Metrics/MethodLength:
84
+ Max: 12
84
85
  Exclude:
85
86
  - lib/polyphony/http/server/rack.rb
86
87
  - lib/polyphony/extensions/io.rb
@@ -111,6 +112,7 @@ Style/Documentation:
111
112
  Exclude:
112
113
  - test/**/*.rb
113
114
  - examples/**/*.rb
115
+ - lib/polyphony/adapters/**/*.rb
114
116
 
115
117
  Style/FormatString:
116
118
  Exclude:
@@ -172,4 +174,8 @@ Style/RedundantRegexpEscape:
172
174
  Enabled: true
173
175
 
174
176
  Style/SlicingWithRange:
175
- Enabled: true
177
+ Enabled: true
178
+
179
+ Style/RaiseArgs:
180
+ Exclude:
181
+ - lib/polyphony/extensions/fiber.rb
@@ -1,3 +1,10 @@
1
+ ## 0.45.0
2
+
3
+ * Cleanup code
4
+ * Rename `Agent` to `Backend`
5
+ * Implement `Polyphony::ConditionVariable`
6
+ * Fix Kernel.system
7
+
1
8
  ## 0.44.0 2020-07-25
2
9
 
3
10
  * Fix reentrant `ResourcePool` (#38)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.44.0)
4
+ polyphony (0.45.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -11,6 +11,7 @@ GEM
11
11
  ansi (1.5.0)
12
12
  ast (2.4.0)
13
13
  builder (3.2.4)
14
+ coderay (1.1.3)
14
15
  colorator (1.1.0)
15
16
  concurrent-ruby (1.1.6)
16
17
  docile (1.3.2)
@@ -22,9 +23,6 @@ GEM
22
23
  forwardable-extended (2.6.0)
23
24
  hiredis (0.6.3)
24
25
  http_parser.rb (0.6.0)
25
- httparty (0.17.0)
26
- mime-types (~> 3.0)
27
- multi_xml (>= 0.5.2)
28
26
  i18n (0.9.5)
29
27
  concurrent-ruby (~> 1.0)
30
28
  jekyll (3.8.6)
@@ -60,18 +58,14 @@ GEM
60
58
  listen (3.2.1)
61
59
  rb-fsevent (~> 0.10, >= 0.10.3)
62
60
  rb-inotify (~> 0.9, >= 0.9.10)
63
- localhost (1.1.4)
64
61
  mercenary (0.3.6)
65
- mime-types (3.3.1)
66
- mime-types-data (~> 3.2015)
67
- mime-types-data (3.2019.1009)
62
+ method_source (1.0.0)
68
63
  minitest (5.13.0)
69
64
  minitest-reporters (1.4.2)
70
65
  ansi
71
66
  builder
72
67
  minitest (>= 5.0)
73
68
  ruby-progressbar
74
- multi_xml (0.6.0)
75
69
  mysql2 (0.5.3)
76
70
  parallel (1.19.1)
77
71
  parser (2.7.0.2)
@@ -79,7 +73,11 @@ GEM
79
73
  pathutil (0.16.2)
80
74
  forwardable-extended (~> 2.6)
81
75
  pg (1.1.4)
76
+ pry (0.13.1)
77
+ coderay (~> 1.1)
78
+ method_source (~> 1.0)
82
79
  public_suffix (4.0.3)
80
+ rack (2.2.3)
83
81
  rainbow (3.0.0)
84
82
  rake (12.3.3)
85
83
  rake-compiler (1.0.5)
@@ -124,17 +122,17 @@ PLATFORMS
124
122
  DEPENDENCIES
125
123
  hiredis (= 0.6.3)
126
124
  http_parser.rb (~> 0.6.0)
127
- httparty (= 0.17.0)
128
125
  jekyll (~> 3.8.6)
129
126
  jekyll-remote-theme (~> 0.4.1)
130
127
  jekyll-seo-tag (~> 2.6.1)
131
128
  just-the-docs (~> 0.3.0)
132
- localhost (= 1.1.4)
133
129
  minitest (= 5.13.0)
134
130
  minitest-reporters (= 1.4.2)
135
131
  mysql2 (= 0.5.3)
136
132
  pg (= 1.1.4)
137
133
  polyphony!
134
+ pry (= 0.13.1)
135
+ rack (>= 2.0.8, < 2.3.0)
138
136
  rake-compiler (= 1.0.5)
139
137
  redis (= 4.1.0)
140
138
  rubocop (= 0.85.1)
data/Rakefile CHANGED
@@ -20,7 +20,7 @@ task :stress_test do
20
20
  end
21
21
 
22
22
  task :docs do
23
- exec 'RUBYOPT=-W0 jekyll serve -s docs -H ec2-35-158-110-38.eu-central-1.compute.amazonaws.com'
23
+ exec 'RUBYOPT=-W0 jekyll serve -s docs -H ec2-18-156-117-172.eu-central-1.compute.amazonaws.com'
24
24
  end
25
25
 
26
26
  CLEAN.include "**/*.o", "**/*.so", "**/*.bundle", "**/*.jar", "pkg", "tmp"
data/TODO.md CHANGED
@@ -1,11 +1,16 @@
1
- - Adapter for Pry, or maybe just an adapter for readline
2
- - Rename Agent to Backend
1
+ 0.45
2
+
3
3
  - Review all code
4
4
  - Cleanup C code
5
- - Cleanup Ruby code (use Rubocop)
6
5
  - Cleanup and annotate examples (and remove all the examples used for
7
6
  debugging). Focus on examples that serve as "how-to".
8
7
 
8
+ 0.45.1
9
+
10
+ - Adapter for Pry and IRB (Which fixes #5 and #6)
11
+
12
+ 0.46.0
13
+
9
14
  - Debugging
10
15
  - Eat your own dogfood: need a good tool to check what's going on when some
11
16
  test fails
@@ -118,7 +123,7 @@
118
123
  - discuss using `snooze` for ensuring responsiveness when executing CPU-bound work
119
124
 
120
125
 
121
- ## 0.45
126
+ ## 0.47
122
127
 
123
128
  ### Some more API work, more docs
124
129
 
@@ -131,13 +136,13 @@
131
136
  - proceed from there
132
137
 
133
138
 
134
- ## 0.46
139
+ ## 0.48
135
140
 
136
141
  ### Sinatra / Sidekiq
137
142
 
138
143
  - Pull out redis/postgres code, put into new `polyphony-xxx` gems
139
144
 
140
- ## 0.47
145
+ ## 0.49
141
146
 
142
147
  ### Testing && Docs
143
148
 
@@ -149,7 +154,7 @@
149
154
  - `IO.foreach`
150
155
  - `Process.waitpid`
151
156
 
152
- ## 0.48 DNS
157
+ ## 0.50 DNS
153
158
 
154
159
  ### DNS client
155
160
 
@@ -0,0 +1,77 @@
1
+ # Polyphony 0.44.1
2
+
3
+ ## More performance, more compatibility, more robustness
4
+
5
+ The last three weeks have been very busy for Polyphony. Since I first presented
6
+ Polyphony here and elsewhere, 17 issues were closed, 10 pull requests were
7
+ merged, and 144 commits were made by 4 different authors. I'm really
8
+ excited about Polyphony and the momentum it seems to be gathering. Your
9
+ reactions have been very positive so far (it even got [tweeted by
10
+ Matz!](https://twitter.com/yukihiro_matz/status/1279289318083715073))
11
+
12
+ I'm even more excited about the contributions Polyphony is starting to get from
13
+ other developers. Thank you [Will](https://github.com/wjordan),
14
+ [Máximo](https://github.com/ElMassimo) and [Trent](https://github.com/misfo) for
15
+ your valuable contributions! Also, the Polyphony project has now got a logo
16
+ designed by my friend [Gérald Morales](https://webocube.com/).
17
+
18
+ I'd like to encourage other developers to get in on the action and start
19
+ contributing by testing Polyphony, creating issues and writing code and
20
+ documentation. Together we can make Polyphony a game-changer for developing
21
+ concurrent apps in Ruby, and finally put to rest the notion that "Ruby is slow"!
22
+
23
+ Since the last public release of Polyphony, we have focused on fixing bugs,
24
+ improving performance and introducing new features that improve the Polyphony
25
+ developer experience. Polyphony 0.44 is up to 20% percent faster than the
26
+ previous release, due notably to a new ring-buffer implementation used by the
27
+ fiber run queue and the `Polyphony::Queue` class, a new `Backend#read_loop` API
28
+ for tighter server loops, and minimizing `fcntl` syscalls when doing I/O. These
29
+ and other minor improvements have resulted in Polyphony first crossing the
30
+ 50,000 requests per second threshold for the first time in a minimal [rack
31
+ server
32
+ example](https://github.com/digital-fabric/polyphony/blob/master/examples/io/xx-rack_server.rb).
33
+
34
+ Notable new features include a MySQL adapter, a Sequel adapter, and a new
35
+ `Fiber#interject` API that allows executing arbitrary code on arbitrary fibers.
36
+
37
+ We have also fixed numerous bugs, among which an issue building Polyphony on
38
+ MacOS, problems issuing `Net::HTTP` requests with secure URLs, an issue with
39
+ `YAML.load` and much more...
40
+
41
+ For the full list of changes please consult the [change log](https://github.com/digital-fabric/polyphony/blob/master/CHANGELOG.md).
42
+
43
+ ## What's next for Polyphony?
44
+
45
+ The next release of Polyphony will focus on full support IRB and Pry. Being able
46
+ to run operations in the background in IRB and Pry can be very beneficial, most
47
+ of all when developing and when debugging running processes using `binding.pry`
48
+ for example.
49
+
50
+ Subsequent releases will introduce a whole new full-featured debugger for
51
+ fiber-aware concurrent apps, and eventually full support for Sequel, Sinatra,
52
+ Hanami, Sidekiq and other major areas of the Ruby ecosystem.
53
+
54
+ ## Tipi - a polyphonic web server for Ruby
55
+
56
+ [Tipi](https://github.com/digital-fabric/tipi) is a new web server for Ruby
57
+ apps. It is intended to be *the* go-to app server for Ruby apps looking for
58
+ robustness, scalability and performance. Tipi already supports HTTP/1, HTTP/2,
59
+ WebSockets and SSL termination. It can currently drive simple Rack apps. In the
60
+ future Tipi will be fully compliant with the Rack specification, and will also
61
+ offer a static file server, a rich configuration and automatic TLS certificates
62
+ (using Let's Encrypt) out of the box.
63
+
64
+ For those wondering about performance, here are some preliminary numbers (see
65
+ disclaimer below):
66
+
67
+ - HTTP, hello world, single process: ~50000 requests/second
68
+ - HTTP, Rack hello world app, single process: ~33000 requests/second
69
+ - HTTP, Rack hello world, 4 worker processes: ~95000 requests/second
70
+ - HTTPS, hello world, single process: ~20000 requests/second
71
+ - HTTPS, hello world, 4 worker processes: ~72000 requests/second
72
+
73
+ Disclaimer: these numbers should be taken with a grain of salt. They do not
74
+ follow any established benchmarking methodology, and may vary significantly. The
75
+ different configurtations were benchmarked using the command: `wrk -d10 -t1 -c10
76
+ "<http|https>://127.0.0.1:1234/"` on the same machine (an `m2.xlarge` instance)
77
+ as the server. In the future Tipi's performance might substantially change. YMMV.
@@ -12,7 +12,7 @@ Polyphony enhances the core `Thread` class with APIs for switching and
12
12
  scheduling fibers, and reimplements some of its APIs such as `Thread#raise`
13
13
  using fibers which, incidentally, make it safe.
14
14
 
15
- Each thread has its own run queue and its own system agent. While running
15
+ Each thread has its own run queue and its own system backend. While running
16
16
  multiple threads does not result in true parallelism in MRI Ruby, sometimes
17
17
  multithreading is inevitable, for instance when using third-party gems that
18
18
  spawn threads, or when calling blocking APIs that are not fiber-aware.
@@ -341,25 +341,25 @@ move_on_after(10) { perform_query }
341
341
  cancel_after(10) { perform_query }
342
342
  ```
343
343
 
344
- ## The System Agent
344
+ ## The Polyphony Backend
345
345
 
346
346
  In order to implement automatic fiber switching when performing blocking
347
- operations, Polyphony introduces a concept called the *system agent*. The system
348
- agent is an object having a uniform interface, that performs all blocking
347
+ operations, Polyphony introduces a concept called the *system backend*. The system
348
+ backend is an object having a uniform interface, that performs all blocking
349
349
  operations.
350
350
 
351
351
  While a standard event loop-based solution would implement a blocking call
352
- separately from the fiber scheduling, the system agent integrates the two to
352
+ separately from the fiber scheduling, the system backend integrates the two to
353
353
  create a blocking call that is already knows how to switch and schedule fibers.
354
354
  For example, in Polyphony all APIs having to do with reading from files or
355
- sockets end up calling `Thread.current.agent.read`, which does all the work.
355
+ sockets end up calling `Thread.current.backend.read`, which does all the work.
356
356
 
357
357
  This design offers some major advantages over other designs. It minimizes memory
358
358
  allocations, of both Ruby objects and C structures. For example, instead of
359
359
  having to allocate libev watchers on the heap and then pass them around, they
360
360
  are allocated on the stack instead, which saves up on both memory and CPU cycles.
361
361
 
362
- In addition, the agent interface includes two methods that allow maximizing
362
+ In addition, the backend interface includes two methods that allow maximizing
363
363
  server performance by accepting connections and reading from sockets in a tight
364
364
  loop. Here's a naive implementation of an HTTP/1 server:
365
365
 
@@ -372,7 +372,7 @@ def handle_client(socket)
372
372
  reqs = []
373
373
  parser.on_message_complete = proc { |env| reqs << { foo: :bar } }
374
374
 
375
- Thread.current.agent.read_loop(socket) do |data|
375
+ Thread.current.backend.read_loop(socket) do |data|
376
376
  parser << data
377
377
  reqs.each { |r| reply(socket, r) }
378
378
  reqs.clear
@@ -388,20 +388,20 @@ end
388
388
  server = TCPServer.open('0.0.0.0', 1234)
389
389
  puts "listening on port 1234"
390
390
 
391
- Thread.current.agent.accept_loop(server) do |client|
391
+ Thread.current.backend.accept_loop(server) do |client|
392
392
  spin { handle_client(client) }
393
393
  end
394
394
  ```
395
395
 
396
- The `#read_loop` and `#accept_loop` agent methods implement tight loops that
396
+ The `#read_loop` and `#accept_loop` backend methods implement tight loops that
397
397
  provide a significant boost to performance (up to +30% better throughput.)
398
398
 
399
- Currently, Polyphony includes a single system agent based on
399
+ Currently, Polyphony includes a single system backend based on
400
400
  [libev](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod). In the future,
401
- Polyphony will include other platform-specific system agents, such as a Windows
402
- agent using
401
+ Polyphony will include other platform-specific system backends, such as a Windows
402
+ backend using
403
403
  [IOCP](https://docs.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports),
404
- or an [io_uring](https://unixism.net/loti/what_is_io_uring.html) agent,
404
+ or an [io_uring](https://unixism.net/loti/what_is_io_uring.html) backend,
405
405
  which might be a game-changer for writing highly-concurrent Ruby-based web apps.
406
406
 
407
407
  ## Writing Web Apps with Polyphony
@@ -482,5 +482,5 @@ reach version 1.0. Here are some of the exciting directions we're working on.
482
482
 
483
483
  - Support for more core and stdlib APIs
484
484
  - More adapters for gems with C-extensions, such as `mysql`, `sqlite3` etc
485
- - Use `io_uring` agent as alternative to the libev agent
485
+ - Use `io_uring` backend as alternative to the libev backend
486
486
  - More concurrency constructs for building highly concurrent applications
@@ -123,7 +123,7 @@ suspend # The main fiber suspends, waiting for all other work to finish
123
123
  sleep 1 # The sleeper fiber goes to sleep
124
124
  Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
125
125
  Thread.current.switch_fiber # Polyphony looks for other runnable fibers
126
- Thread.current.agent.poll # With no work left, the event loop is ran
126
+ Thread.current.backend.poll # With no work left, the event loop is ran
127
127
  fiber.schedule # The timer event fires, scheduling the sleeper fiber
128
128
  # <= The sleep method returns
129
129
  puts "Woke up"
@@ -27,17 +27,17 @@ class Test
27
27
 
28
28
  def test_file
29
29
  f = File.open(__FILE__, 'r')
30
- puts Thread.current.agent.read(f, +'', 10000, true)
30
+ puts Thread.current.backend.read(f, +'', 10000, true)
31
31
 
32
- Thread.current.agent.write(STDOUT, "Write something: ")
32
+ Thread.current.backend.write(STDOUT, "Write something: ")
33
33
  str = +''
34
- Thread.current.agent.read(STDIN, str, 5, false)
34
+ Thread.current.backend.read(STDIN, str, 5, false)
35
35
  puts str
36
36
  end
37
37
 
38
38
  def test_fork
39
39
  pid = fork do
40
- Thread.current.agent.post_fork
40
+ Thread.current.backend.post_fork
41
41
  puts 'child going to sleep'
42
42
  sleep 1
43
43
  puts 'child done sleeping'
@@ -45,7 +45,7 @@ class Test
45
45
  end
46
46
 
47
47
  puts "Waiting for pid #{pid}"
48
- result = Thread.current.agent.waitpid(pid)
48
+ result = Thread.current.backend.waitpid(pid)
49
49
  puts "Done waiting"
50
50
  p result
51
51
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require 'polyphony/adapters/readline'
6
+ require 'pry'
7
+
8
+ $counter = 0
9
+ timer = spin do
10
+ throttled_loop(5) do
11
+ $counter += 1
12
+ end
13
+ end
14
+
15
+ at_exit { timer.stop }
16
+
17
+ puts 'try typing $counter to see the counter incremented in the background'
18
+ binding.pry
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require 'http/parser'
6
+ require 'rack'
7
+
8
+ module RackAdapter
9
+ class << self
10
+ def run(app)
11
+ ->(socket, req) { respond(socket, req, app.(env(req))) }
12
+ end
13
+
14
+ def env(req)
15
+ {}
16
+ end
17
+
18
+ def respond(socket, request, (status_code, headers, body))
19
+ body = body.join
20
+ headers = "Content-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\n"
21
+ socket.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{body}"
22
+ end
23
+ end
24
+ end
25
+
26
+ $connection_count = 0
27
+
28
+ def handle_client(socket, &handler)
29
+ $connection_count += 1
30
+ parser = Http::Parser.new
31
+ reqs = []
32
+ parser.on_message_complete = proc do |env|
33
+ reqs << Object.new # parser
34
+ end
35
+ socket.read_loop do |data|
36
+ parser << data
37
+ while (req = reqs.shift)
38
+ handler.call(socket, req)
39
+ req = nil
40
+ end
41
+ end
42
+ rescue IOError, SystemCallError => e
43
+ # do nothing
44
+ ensure
45
+ $connection_count -= 1
46
+ socket&.close
47
+ end
48
+
49
+ def handle_request(client, parser)
50
+ status_code = "200 OK"
51
+ data = "Hello world!\n"
52
+ headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
53
+ client.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
54
+ end
55
+
56
+ server = TCPServer.open('0.0.0.0', 1234)
57
+ puts "pid #{Process.pid}"
58
+ puts "listening on port 1234"
59
+
60
+ app = RackAdapter.run(lambda { |env|
61
+ [
62
+ 200,
63
+ {"Content-Type" => "text/plain"},
64
+ ["Hello, world!\n"]
65
+ ]
66
+ })
67
+
68
+ loop do
69
+ client = server.accept
70
+ spin { handle_client(client, &app) }
71
+ end