riser 0.1.0

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/README.md ADDED
@@ -0,0 +1,926 @@
1
+ Riser
2
+ =====
3
+
4
+ **RISER** is a library of **R**uby **I**nfrastructure for cooperative
5
+ multi-thread/multi-process **SER**ver.
6
+
7
+ This library is useful for the following.
8
+
9
+ * To make a server with tcp/ip or unix domain socket
10
+ * To select a method to execute server from:
11
+ - Single process multi-thread
12
+ - Preforked multi-process multi-thread
13
+ * To make a daemon that will be controlled by signal(2)s
14
+ * To separate the object not divided into multiple processes from
15
+ server process(es) into backend service process
16
+
17
+ This library supposes that the user is familiar with the unix process
18
+ model and socket programming.
19
+
20
+ Installation
21
+ ------------
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'riser'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install riser
36
+
37
+ Usage
38
+ -----
39
+
40
+ ### Simple Server Example
41
+
42
+ An example of a simple server is as follows.
43
+
44
+ ```ruby
45
+ require 'riser'
46
+ require 'socket'
47
+
48
+ server = Riser::SocketServer.new
49
+ server.dispatch{|socket|
50
+ while (line = socket.gets)
51
+ socket.write(line)
52
+ end
53
+ }
54
+
55
+ server_socket = TCPServer.new('localhost', 5000)
56
+ server.start(server_socket)
57
+ ```
58
+
59
+ This simple server is an echo server that accepts connections at port
60
+ number 5000 on localhost and returns the input line as is. The object
61
+ of `Riser::SocketServer` is the core of riser. What this example does
62
+ is as follows.
63
+
64
+ 1. Create a new server object of `Riser::SocketServer`.
65
+ 2. Register `dispatch` callback to the server object.
66
+ 3. Open a tcp/ip server socket.
67
+ 4. Pass the server socket to the server object and `start` the server.
68
+
69
+ In this example tcp/ip socket is used, but the server will also work
70
+ with unix domain socket. By rewriting the `dispatch` callback you can
71
+ make the server do what you want. Although this example is
72
+ simplified, error handling is actually required. If an exception is
73
+ thrown to outside of the `dispatch` callback, the server stops, so it
74
+ should be avoided. See the
75
+ '[halo.rb](https://github.com/y10k/riser/blob/master/example/halo.rb)'
76
+ example for practical code.
77
+
78
+ By default, the server performs `dispatch` callback on multi-thread.
79
+ On Linux, you can see that running the server with 4 threads
80
+ (`{ruby}`) by executing `pstree` command.
81
+
82
+ ```
83
+ $ pstree -ap
84
+ ...
85
+ | `-bash,23355
86
+ | `-ruby,30674 simple_server.rb
87
+ | |-{ruby},30675
88
+ | |-{ruby},30676
89
+ | |-{ruby},30677
90
+ | `-{ruby},30678
91
+ ...
92
+ ```
93
+
94
+ ### Server Attributes
95
+
96
+ The server has attributes, and setting attributes changes the behavior
97
+ of the server. Let's take an example `process_num` attribute. The
98
+ `process_num` attribute is `0` by default, but by setting it will run
99
+ the server with multi-process.
100
+
101
+ In the following example, `process_num` is set to `2`.
102
+ Others are the same as simple server example.
103
+
104
+ ```ruby
105
+ require 'riser'
106
+ require 'socket'
107
+
108
+ server = Riser::SocketServer.new
109
+ server.process_num = 2
110
+ server.dispatch{|socket|
111
+ while (line = socket.gets)
112
+ socket.write(line)
113
+ end
114
+ }
115
+
116
+ server_socket = TCPServer.new('localhost', 5000)
117
+ server.start(server_socket)
118
+ ```
119
+
120
+ Running the example will hardly change the appearance, but the server
121
+ is running with multi-process. You can see that the server is running
122
+ in multiple processes with `pstree` command.
123
+
124
+ ```
125
+ $ pstree -ap
126
+ ...
127
+ | `-bash,23355
128
+ | `-ruby,31283 multiproc_server.rb
129
+ | |-ruby,31284 multiproc_server.rb
130
+ | | |-{ruby},31285
131
+ | | |-{ruby},31286
132
+ | | |-{ruby},31287
133
+ | | `-{ruby},31288
134
+ | |-ruby,31289 multiproc_server.rb
135
+ | | |-{ruby},31292
136
+ | | |-{ruby},31293
137
+ | | |-{ruby},31294
138
+ | | `-{ruby},31295
139
+ | |-{ruby},31290
140
+ | `-{ruby},31291
141
+ ...
142
+ ```
143
+
144
+ There are 2 child processes (`|-ruby`) under the parent process
145
+ (`` `-ruby``) having 2 threads , and 4 threads are running for each
146
+ child process. The architecture of riser's multi-process server is
147
+ the passing file descriptors between parent-child processes. The
148
+ parent process accepts the connection and passes it to threads, each
149
+ thread passes the connection to the child process, and `dispatch`
150
+ callback is performed in the thread of each child process.
151
+
152
+ In addition to `process_num`, the server object has various attributes
153
+ such as `thread_num`. See the source code
154
+ ([server.rb: Riser::SocketServer](https://github.com/y10k/riser/blob/master/lib/riser/server.rb))
155
+ for details of other attributes.
156
+
157
+ ### Daemon
158
+
159
+ Riser provids the function to daemonize server. By daemonizing the
160
+ server, the server will be able to receive signal(2)s and restart. An
161
+ example of a simple daemon is as follows.
162
+
163
+ ```ruby
164
+ require 'riser'
165
+
166
+ Riser::Daemon.start_daemon(daemonize: true,
167
+ daemon_name: 'simple_daemon',
168
+ status_file: 'simple_daemon.pid',
169
+ listen_address: 'localhost:5000'
170
+ ) {|server|
171
+
172
+ server.dispatch{|socket|
173
+ while (line = socket.gets)
174
+ socket.write(line)
175
+ end
176
+ }
177
+ }
178
+ ```
179
+
180
+ To daemonize the server, use the module function of
181
+ `Riser::Daemon.start_daemon`. The `start_daemon` function takes
182
+ parameters in a hash table and works. The works of `start_daemon` are
183
+ as follows.
184
+
185
+ 1. Daemonize the server process (`daemonize: true`).
186
+ 2. Output syslog(2) identified with `simple_daemon`
187
+ (`daemon_name: 'simple_daemon'`).
188
+ 3. Output process id to the file of `simple_daemon.pid` and lock it
189
+ exclusively (`status_file: 'simple_daemon.pid'`).
190
+ 4. Open the tcp/ip server socket of `localhost:5000`
191
+ (`listen_address: 'localhost:5000'`).
192
+ 5. Create a server object and pass it to the block, and you set up the
193
+ server object in the block, then `start` the server object.
194
+
195
+ A command prompt is displayed as soon as you start the daemon, but the
196
+ daemon runs in the background and logs to syslog(2). Daemonization is
197
+ the result of `daemonaize: true`. If `daemonaize: false` is set, the
198
+ process is not daemonized, starts in foreground and logs to standard
199
+ output. This is useful for debugging daemon.
200
+
201
+ Looking at the process of the daemon with `pstree` command is as
202
+ follows.
203
+
204
+ ```
205
+ $ pstree -ap
206
+ init,1 ro
207
+ |-ruby,32187 simple_daemon.rb
208
+ | `-ruby,32188 simple_daemon.rb
209
+ | |-{ruby},32189
210
+ | |-{ruby},32190
211
+ | |-{ruby},32191
212
+ | `-{ruby},32192
213
+ ...
214
+ ```
215
+
216
+ The daemon process is running as the parent of the server process.
217
+ And the daemon process is running independently under the init(8)
218
+ process. The daemon process monitors the server process and restarts
219
+ when the server process dies. Also, the daemon process receives some
220
+ signal(2)s and stops or restarts the server process, and does other
221
+ things.
222
+
223
+ ### Signal(2)s and Other Daemon Parameters
224
+
225
+ By default, the daemon is able to receive the following signal(2)s.
226
+
227
+ |signal(2)|daemon's action |`start_daemon` parameter |
228
+ |---------|--------------------------------------|---------------------------|
229
+ |`TERM` |stop server gracefully |`signal_stop_graceful` |
230
+ |`INT` |stop server forcedly |`signal_stop_forced` |
231
+ |`HUP` |restart server gracefully |`signal_restart_graceful` |
232
+ |`QUIT` |restart server forcedly |`signal_restart_forced` |
233
+ |`USR1` |get queue stat and reset queue stat |`signal_stat_get_and_reset`|
234
+ |`USR2` |get queue stat and no reset queue stat|`signal_stat_get_no_reset` |
235
+ |`WINCH` |stop queue stat |`signal_stat_stop` |
236
+
237
+ By setting the parameters of the `start_daemon`, you can change the
238
+ signal(2) which triggers the action. Setting the parameter to `nil`
239
+ will disable the action. 'Queue stat' is explained later.
240
+
241
+ The `start_daemon` has other parameters. See the source code
242
+ ([daemon.rb: Riser::Daemon::DEFAULT](https://github.com/y10k/riser/blob/master/lib/riser/daemon.rb))
243
+ for details of other parameters.
244
+
245
+ ### Server Callbacks
246
+
247
+ The server object is able to register callbacks other than `dispatch`.
248
+ The list of server objects' callbacks is as follows.
249
+
250
+ |callback |description |
251
+ |-------------------------------------------------|-------------------------------------------------------------------------------------------------------|
252
+ |<code>before_start{&#124;server_socket&#124; ...}</code>|performed before starting the server. in a multi-process server, it is performed in the parent process.|
253
+ |`at_fork{ ... }` |performed after fork(2)ing on the multi-process server. it is performed in the child process. |
254
+ |<code>at_stop{&#124;stop_state&#124; ... }</code>|performed when a stop signal(2) is received. in a multi-process server, it is performed in the child process.|
255
+ |<code>at_stat{&#124;stat_info&#124; ... }</code> |performed when 'get stat' signal(2) is received. |
256
+ |`preprocess{ ... }` |performed before starting 'dispatch loop'. in a multi-process server, it is performed in the child process.|
257
+ |`postprocess{ ... }` |performed after 'dispatch loop' is finished. in a multi-process server, it is performed in the child process.|
258
+ |`after_stop{ ... }` |performed after the server stop. in a multi-process server, it is performed in the parent process. |
259
+ |<code>dispatch{&#124;socket&#124; ... }</code> |known dispatch callback. in a multi-process server, it is performed in the child process. |
260
+
261
+ It seems necessary to explain the `at_stat` callback. Riser uses
262
+ queues to distribute connections to threads and processes, and it is
263
+ possible to get statistics information on queues. With `USR1` and
264
+ `USR2` signal(2)s, you can start collecting queue statistics
265
+ information and get it. At that time the `at_stat` callback is called
266
+ and used to write queue statistics informations to log etc. With the
267
+ `WINCH` signal(2), you can stop collecting queue statistics
268
+ information.
269
+
270
+ For a example of how to use callbacks, see the source code of the
271
+ '[halo.rb](https://github.com/y10k/riser/blob/master/example/halo.rb)'
272
+ example.
273
+
274
+ ### Server Utilities
275
+
276
+ Riser provides some useful utilities to write a server.
277
+
278
+ |utility |description |
279
+ |--------------------------|-------------------------|
280
+ |`Riser::ReadPoll` |monitor I/O timeout. |
281
+ |`Riser::WriteBufferStream`|buffer I/O writes. |
282
+ |`Riser::LoggingStream` |log I/O read / write. |
283
+
284
+ For a example of how to use utilities, see the source code of the
285
+ '[halo.rb](https://github.com/y10k/riser/blob/master/example/halo.rb)'
286
+ example. Also utilities are simple, so check the source codes of
287
+ '[poll.rb](https://github.com/y10k/riser/blob/master/lib/riser/poll.rb)'
288
+ and
289
+ '[stream.rb](https://github.com/y10k/riser/blob/master/lib/riser/stream.rb)'.
290
+
291
+ ### TLS Server
292
+
293
+ With OpenSSL, the riser is able to provide a TLS server. To provide a
294
+ TLS server you need a certificate and private key. An example of a
295
+ simple TLS server is as follows.
296
+
297
+ ```ruby
298
+ require 'openssl'
299
+ require 'riser'
300
+
301
+ cert_path = ARGV.shift or abort('need for server certificate file')
302
+ pkey_path = ARGV.shift or abort('need for server private key file')
303
+
304
+ Riser::Daemon.start_daemon(daemonize: false,
305
+ daemon_name: 'simple_tls',
306
+ listen_address: 'localhost:5000'
307
+ ) {|server|
308
+
309
+ ssl_context = OpenSSL::SSL::SSLContext.new
310
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
311
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.read(pkey_path))
312
+
313
+ server.dispatch{|socket|
314
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
315
+ ssl_socket.accept
316
+ while (line = ssl_socket.gets)
317
+ ssl_socket.write(line)
318
+ end
319
+ ssl_socket.close
320
+ }
321
+ }
322
+ ```
323
+
324
+ An example of the result of connecting to the TLS server from OpenSSL
325
+ client is as follows.
326
+
327
+ ```
328
+ $ openssl s_client -CAfile local_ca.cert -connect localhost:5000
329
+ CONNECTED(00000003)
330
+ depth=1 C = JP, ST = Tokyo, L = Tokyo, O = Private, OU = Home, CN = *
331
+ verify return:1
332
+ depth=0 C = JP, ST = Tokyo, L = Tokyo, O = Private, OU = Home, CN = localhost
333
+ verify return:1
334
+ ---
335
+ Certificate chain
336
+ 0 s:/C=JP/ST=Tokyo/L=Tokyo/O=Private/OU=Home/CN=localhost
337
+ i:/C=JP/ST=Tokyo/L=Tokyo/O=Private/OU=Home/CN=*
338
+ ---
339
+ Server certificate
340
+ -----BEGIN CERTIFICATE-----
341
+ MIIDODCCAiACCQCks7GdVjzAmDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJK
342
+ UDEOMAwGA1UECAwFVG9reW8xDjAMBgNVBAcMBVRva3lvMRAwDgYDVQQKDAdQcml2
343
+ YXRlMQ0wCwYDVQQLDARIb21lMQowCAYDVQQDDAEqMB4XDTE5MDExNTA4MzYzMloX
344
+ DTI5MDExMjA4MzYzMlowYjELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ4w
345
+ DAYDVQQHDAVUb2t5bzEQMA4GA1UECgwHUHJpdmF0ZTENMAsGA1UECwwESG9tZTES
346
+ MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
347
+ AQEAvsrEIm1Unna7KM4U45ibGG/A4pnEScMymaLoitbVr5wAzvn/Oj2UkRO0gzQl
348
+ tLh28+jKh1eIlg60jyJ+QqpRCDWXXkEKXaAETbpYK1dlGE3ORI1VdTe/tYlpFxdd
349
+ Bzq//pQVNnYw6I+eu+VNIGroI7rWybsvpwPXgqaiyFlmrP9i8VdZKvKketc+NNwt
350
+ Chf81NJ9I1ue0cFZz+bMI84xhulVfxPi1avoXy0Ai+FM4Zqao5dkkKbmgia6R34e
351
+ J9P7FIGYHypj988fRVs2Pqprh60Zx32oJsLRZzgeiIUqkim3fWDs0TydxAuG6Owl
352
+ XgyCsdTGvwPM9ZQJQgczJsJCNwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCt0lUl
353
+ X1b+r7xAnnBdmxYfIkEoMeEhe5VUB+/Onixb8C3sIdzM8PdXo43OKe/lb9kKY7Gz
354
+ JQMFgrD4jc53mygU4K5gBXKZYOC3/NDNyqSr+22VHMqSD/pImjVFZ9E69gyqVXJ5
355
+ mQBUWgUU4QhpgMnOi0HsN1bpjTiHEaCo7ODlNtF3fhj0bC5CzofxnNMUjTJAn8Rh
356
+ A+fj/6dtDP+lMX//QkjtHOdVafKN8BJRrZg/DliGrqpUKW8h3NxCjGLeG5rFnVVj
357
+ qPFc7IbH25KMLMDCJ3xrqBVtOOEjdTFKbfqOo58HZD7f/PYdQ0XHpG+/f6s+TgTl
358
+ L+yNZF+/WlW7/020
359
+ -----END CERTIFICATE-----
360
+ subject=/C=JP/ST=Tokyo/L=Tokyo/O=Private/OU=Home/CN=localhost
361
+ issuer=/C=JP/ST=Tokyo/L=Tokyo/O=Private/OU=Home/CN=*
362
+ ---
363
+ No client certificate CA names sent
364
+ Peer signing digest: SHA512
365
+ Server Temp Key: X25519, 253 bits
366
+ ---
367
+ SSL handshake has read 1453 bytes and written 269 bytes
368
+ Verification: OK
369
+ ---
370
+ New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
371
+ Server public key is 2048 bit
372
+ Secure Renegotiation IS supported
373
+ Compression: NONE
374
+ Expansion: NONE
375
+ No ALPN negotiated
376
+ SSL-Session:
377
+ Protocol : TLSv1.2
378
+ Cipher : ECDHE-RSA-AES256-GCM-SHA384
379
+ Session-ID: 43427207A2C36F807AF5BDCB69EF26F18758A5BAC5C4867C04B17E1C1F6CAE9D
380
+ Session-ID-ctx:
381
+ Master-Key: 7BE6C8E0108A6A2F9B2B6AC4DB8360EE375A950D2EB4CB2B259125FB17BE74F00120F7E7290B7137E16F665F44D8AD20
382
+ PSK identity: None
383
+ PSK identity hint: None
384
+ SRP username: None
385
+ TLS session ticket lifetime hint: 7200 (seconds)
386
+ TLS session ticket:
387
+ 0000 - af bd 28 5e ca d4 ae 61-38 63 ff 68 84 2b 51 13 ..(^...a8c.h.+Q.
388
+ 0010 - d3 c5 7f c7 72 be f5 5c-bd 9e fb f3 88 61 83 01 ....r..\.....a..
389
+ 0020 - a5 11 fe 45 14 c1 9c 9b-79 7b 34 87 c1 66 e1 cd ...E....y{4..f..
390
+ 0030 - 7d f4 ac 62 6e 25 53 c5-35 b2 b2 2c 3c b9 af 89 }..bn%S.5..,<...
391
+ 0040 - cf 11 1d 9c 42 5a 75 86-d1 6d 49 fc e9 6a 39 f0 ....BZu..mI..j9.
392
+ 0050 - fb cf 7d 9a 60 52 10 ad-a3 15 1b ba 00 32 67 e8 ..}.`R.......2g.
393
+ 0060 - 03 ea 74 49 17 46 d8 a2-41 45 17 9d 2c ec 7f 3f ..tI.F..AE..,..?
394
+ 0070 - 89 eb 7e 4a 05 10 a3 81-d2 16 ce c7 da 7d c6 5a ..~J.........}.Z
395
+ 0080 - 9c 50 de a5 ce 8e ca 58-af 0b 94 d2 2a c2 56 da .P.....X....*.V.
396
+ 0090 - 00 05 b9 87 3c 9c 0e 53-70 c2 59 24 ef 0b 0a f3 ....<..Sp.Y$....
397
+
398
+ Start Time: 1549358604
399
+ Timeout : 7200 (sec)
400
+ Verify return code: 0 (ok)
401
+ Extended master secret: yes
402
+ ---
403
+ foo
404
+ foo
405
+ bar
406
+ bar
407
+ DONE
408
+ ```
409
+
410
+ ### dRuby Services
411
+
412
+ Riser has a mechanism that runs the object in a separate process from
413
+ the server process. This mechanism pools the dRuby server processes
414
+ and distributes the object to them in the following 3 patterns.
415
+
416
+ |pattern |description |
417
+ |--------------|---------------------------------------------------------|
418
+ |any process |run the object with a randomly picked process. |
419
+ |single process|always run the object in the same process. |
420
+ |sticky process|run the object in the same process for each specific key.|
421
+
422
+ A simple example of how this mechanism works is as follows.
423
+
424
+ ```ruby
425
+ require 'riser'
426
+
427
+ Riser::Daemon.start_daemon(daemonize: false,
428
+ daemon_name: 'simple_services',
429
+ listen_address: 'localhost:8000'
430
+ ) {|server|
431
+
432
+ services = Riser::DRbServices.new(4)
433
+ services.add_any_process_service(:pid_any, proc{ $$ })
434
+ services.add_single_process_service(:pid_single, proc{ $$ })
435
+ services.add_sticky_process_service(:pid_stickty, proc{|key| $$ })
436
+
437
+ server.process_num = 2
438
+ server.before_start{|server_socket|
439
+ services.start_server
440
+ }
441
+ server.at_fork{
442
+ services.detach_server
443
+ }
444
+ server.preprocess{
445
+ services.start_client
446
+ }
447
+ server.dispatch{|socket|
448
+ if (line = socket.gets) then
449
+ method, uri, _version = line.split
450
+ while (line = socket.gets)
451
+ line.strip.empty? and break
452
+ end
453
+ if (method == 'GET') then
454
+ socket << "HTTP/1.0 200 OK\r\n"
455
+ socket << "Content-Type: text/plain\r\n"
456
+ socket << "\r\n"
457
+
458
+ path, query = uri.split('?', 2)
459
+ case (path)
460
+ when '/any'
461
+ socket << 'pid: ' << services.call_service(:pid_any) << "\n"
462
+ when '/single'
463
+ socket << 'pid: ' << services.call_service(:pid_single) << "\n"
464
+ when '/sticky'
465
+ key = query || 'default'
466
+ socket << 'key: ' << key << ', pid: ' << services.call_service(:pid_stickty, key) << "\n"
467
+ else
468
+ socket << "unknown path: #{path}\n"
469
+ end
470
+ end
471
+ end
472
+ }
473
+ server.after_stop{
474
+ services.stop_server
475
+ }
476
+ }
477
+ ```
478
+
479
+ `Riser::DRbServices` is the mechanism for distributing objects. What
480
+ this example does is as follows.
481
+
482
+ 1. Create a object of `Riser::DRbServices` to pool 4 dRuby server
483
+ processes (`Riser::DRbServices.new(4)`).
484
+ 2. Add objects to run in dRuby server process
485
+ (`add_..._process_service`). In this example it is added that the
486
+ procedures that returns the process id to see the process to be
487
+ distributed the object.
488
+ 3. Start dRuby server process with `start_server`. In the case of a
489
+ multi-process server, it is necessary to execute `start_server` in
490
+ the parent process, so it is executed at `before_start` callback.
491
+ 4. In the case of a multi-process server, execute `detach_server` at
492
+ `at_fork` callback to release unnecessary resources in the child
493
+ process.
494
+ 5. Start dRuby client with `start_client`. In the case of a
495
+ multi-process server, it is necessary to execute `start_client` in
496
+ the child process, so it is executed at `preprocess` callback.
497
+ 6. Add the processing of the web service at `dispatch` callback. The
498
+ procedures added to `Riser::DRbServices` are able to be called by
499
+ `call_service`. For object other than procedure, use
500
+ `get_service`.
501
+ 7. Stop dRuby server process with `stop_server`. In the case of a
502
+ multi-process server, it is necessary to execute `stop_server` in
503
+ the parent process, so it is executed at `after_stop` callback.
504
+
505
+ In this example, you can see the dRuby process distribution with a
506
+ simple web service. Looking at the process of this example with
507
+ `pstree` command is as follows.
508
+
509
+ ```
510
+ $ pstree -ap
511
+ ...
512
+ | `-bash,23355
513
+ | `-ruby,3177 simple_services.rb
514
+ | `-ruby,3178 simple_services.rb
515
+ | |-ruby,3179 simple_services.rb
516
+ | | |-{ruby},3180
517
+ | | |-{ruby},3189
518
+ | | `-{ruby},3198
519
+ | |-ruby,3181 simple_services.rb
520
+ | | |-{ruby},3182
521
+ | | |-{ruby},3194
522
+ | | `-{ruby},3202
523
+ | |-ruby,3183 simple_services.rb
524
+ | | |-{ruby},3184
525
+ | | |-{ruby},3197
526
+ | | `-{ruby},3207
527
+ | |-ruby,3185 simple_services.rb
528
+ | | |-{ruby},3186
529
+ | | |-{ruby},3201
530
+ | | `-{ruby},3211
531
+ | |-ruby,3187 simple_services.rb
532
+ | | |-{ruby},3188
533
+ | | |-{ruby},3191
534
+ | | |-{ruby},3195
535
+ | | |-{ruby},3199
536
+ | | |-{ruby},3203
537
+ | | |-{ruby},3205
538
+ | | |-{ruby},3206
539
+ | | |-{ruby},3208
540
+ | | `-{ruby},3209
541
+ | |-ruby,3190 simple_services.rb
542
+ | | |-{ruby},3196
543
+ | | |-{ruby},3200
544
+ | | |-{ruby},3204
545
+ | | |-{ruby},3210
546
+ | | |-{ruby},3212
547
+ | | |-{ruby},3213
548
+ | | |-{ruby},3214
549
+ | | |-{ruby},3215
550
+ | | `-{ruby},3216
551
+ | |-{ruby},3192
552
+ | `-{ruby},3193
553
+ ...
554
+ ```
555
+
556
+ In addition to the 2 server child processes with many threads, there
557
+ are 4 child processes. These 4 child processes are dRuby server
558
+ processes. See the web service's result of 'any process' pattern.
559
+
560
+ ```
561
+ $ curl http://localhost:8000/any
562
+ pid: 3181
563
+ $ curl http://localhost:8000/any
564
+ pid: 3179
565
+ $ curl http://localhost:8000/any
566
+ pid: 3183
567
+ $ curl http://localhost:8000/any
568
+ pid: 3181
569
+ ```
570
+
571
+ In the 'any process' pattern, process ids are dispersed. Next, see
572
+ the web service's result of 'single process' pattern.
573
+
574
+ ```
575
+ $ curl http://localhost:8000/single
576
+ pid: 3179
577
+ $ curl http://localhost:8000/single
578
+ pid: 3179
579
+ $ curl http://localhost:8000/single
580
+ pid: 3179
581
+ $ curl http://localhost:8000/single
582
+ pid: 3179
583
+ ```
584
+
585
+ In the 'single process' pattern, process id is always same. Last, see
586
+ the web service's result of 'sticky process' pattern.
587
+
588
+ ```
589
+ $ curl http://localhost:8000/sticky
590
+ key: default, pid: 3181
591
+ $ curl http://localhost:8000/sticky
592
+ key: default, pid: 3181
593
+ $ curl http://localhost:8000/sticky?foo
594
+ key: foo, pid: 3179
595
+ $ curl http://localhost:8000/sticky?foo
596
+ key: foo, pid: 3179
597
+ $ curl http://localhost:8000/sticky?bar
598
+ key: bar, pid: 3185
599
+ $ curl http://localhost:8000/sticky?bar
600
+ key: bar, pid: 3185
601
+ ```
602
+
603
+ In the 'sticky process' pattern, the same process id will be given for
604
+ each key.
605
+
606
+ ### Local Services
607
+
608
+ Since dRuby's remote process call has overhead, riser is able to
609
+ transparently switch `Riser::DRbServices` to local process call. An
610
+ example of a local process call is as follows.
611
+
612
+ ```ruby
613
+ require 'riser'
614
+
615
+ Riser::Daemon.start_daemon(daemonize: false,
616
+ daemon_name: 'local_services',
617
+ listen_address: 'localhost:8000'
618
+ ) {|server|
619
+
620
+ services = Riser::DRbServices.new(0)
621
+ services.add_any_process_service(:pid_any, proc{ $$ })
622
+ services.add_single_process_service(:pid_single, proc{ $$ })
623
+ services.add_sticky_process_service(:pid_stickty, proc{|key| $$ })
624
+
625
+ server.process_num = 0
626
+ server.before_start{|server_socket|
627
+ services.start_server
628
+ }
629
+ server.at_fork{
630
+ services.detach_server
631
+ }
632
+ server.preprocess{
633
+ services.start_client
634
+ }
635
+ server.dispatch{|socket|
636
+ if (line = socket.gets) then
637
+ method, uri, _version = line.split
638
+ while (line = socket.gets)
639
+ line.strip.empty? and break
640
+ end
641
+ if (method == 'GET') then
642
+ socket << "HTTP/1.0 200 OK\r\n"
643
+ socket << "Content-Type: text/plain\r\n"
644
+ socket << "\r\n"
645
+
646
+ path, query = uri.split('?', 2)
647
+ case (path)
648
+ when '/any'
649
+ socket << 'pid: ' << services.call_service(:pid_any) << "\n"
650
+ when '/single'
651
+ socket << 'pid: ' << services.call_service(:pid_single) << "\n"
652
+ when '/sticky'
653
+ key = query || 'default'
654
+ socket << 'key: ' << key << ', pid: ' << services.call_service(:pid_stickty, key) << "\n"
655
+ else
656
+ socket << "unknown path: #{path}\n"
657
+ end
658
+ end
659
+ end
660
+ }
661
+ server.after_stop{
662
+ services.stop_server
663
+ }
664
+ }
665
+ ```
666
+
667
+ The differences from the previous example is as follows.
668
+
669
+ |code |description |
670
+ |---------------------------|-------------------------------------------------------------------------------------------------------------|
671
+ |`Riser::DRbServices.new(0)`|setting the number of processes to `0` makes local process call without starting the dRuby server processes. |
672
+ |`server.process_num = 0` |since local process call fails if it is a multi-process server, set it to single process multi-thread server.|
673
+
674
+ In this example, there are no dRuby server processes and there is only
675
+ 1 server process. Looking at the process of this example with
676
+ `pstree` command is as follows.
677
+
678
+ ```
679
+ $ pstree -ap
680
+ ...
681
+ | `-bash,23355
682
+ | `-ruby,3854 local_services.rb
683
+ | `-ruby,3855 local_services.rb
684
+ | |-{ruby},3856
685
+ | |-{ruby},3857
686
+ | |-{ruby},3858
687
+ | |-{ruby},3859
688
+ | `-{ruby},3860
689
+ ...
690
+ ```
691
+
692
+ The result of the web service always returns the same process id.
693
+
694
+ ```
695
+ $ curl http://localhost:8000/any
696
+ pid: 3855
697
+ $ curl http://localhost:8000/any
698
+ pid: 3855
699
+ $ curl http://localhost:8000/any
700
+ pid: 3855
701
+ ```
702
+
703
+ ```
704
+ $ curl http://localhost:8000/single
705
+ pid: 3855
706
+ $ curl http://localhost:8000/single
707
+ pid: 3855
708
+ $ curl http://localhost:8000/single
709
+ pid: 3855
710
+ ```
711
+
712
+ ```
713
+ $ curl http://localhost:8000/sticky
714
+ key: default, pid: 3855
715
+ $ curl http://localhost:8000/sticky
716
+ key: default, pid: 3855
717
+ $ curl http://localhost:8000/sticky?foo
718
+ key: foo, pid: 3855
719
+ $ curl http://localhost:8000/sticky?foo
720
+ key: foo, pid: 3855
721
+ $ curl http://localhost:8000/sticky?bar
722
+ key: bar, pid: 3855
723
+ $ curl http://localhost:8000/sticky?bar
724
+ key: bar, pid: 3855
725
+ ```
726
+
727
+ ### dRuby Services Callbacks
728
+
729
+ The object of `Riser::DRbServices` is able to register callbacks.
730
+ The list of callbacks is as follows.
731
+
732
+ |callback |description |
733
+ |--------------------------------------------------|----------------------------------------------------------|
734
+ |<code>at_fork(service_name) {&#124;service_front&#124; ... }</code>|performed when dRuby server process starts with remote process call. ignored by local process call.|
735
+ |<code>preprocess(service_name) {&#124;service_front&#124; ... }</code> |performed before starting the server.|
736
+ |<code>postprocess(service_name) {&#124;service_front&#124; ... }</code>|performed after the server stop. |
737
+
738
+ ### dRuby Services and Resource
739
+
740
+ An example of running a pstore database in a single process without
741
+ collision in a multi-process server is as follows.
742
+
743
+ ```ruby
744
+ require 'pstore'
745
+ require 'riser'
746
+
747
+ Riser::Daemon.start_daemon(daemonize: false,
748
+ daemon_name: 'simple_count',
749
+ listen_address: 'localhost:8000'
750
+ ) {|server|
751
+
752
+ services = Riser::DRbServices.new(1)
753
+ services.add_single_process_service(:pstore, PStore.new('simple_count.pstore', true))
754
+
755
+ server.process_num = 2
756
+ server.before_start{|server_socket|
757
+ services.start_server
758
+ }
759
+ server.at_fork{
760
+ services.detach_server
761
+ }
762
+ server.preprocess{
763
+ services.start_client
764
+ }
765
+ server.dispatch{|socket|
766
+ if (line = socket.gets) then
767
+ method, _uri, _version = line.split
768
+ while (line = socket.gets)
769
+ line.strip.empty? and break
770
+ end
771
+ if (method == 'GET') then
772
+ socket << "HTTP/1.0 200 OK\r\n"
773
+ socket << "Content-Type: text/plain\r\n"
774
+ socket << "\r\n"
775
+
776
+ services.get_service(:pstore).transaction do |pstore|
777
+ pstore[:count] ||= 0
778
+ pstore[:count] += 1
779
+ socket << 'count: ' << pstore[:count] << "\n"
780
+ end
781
+ end
782
+ end
783
+ }
784
+ server.after_stop{
785
+ services.stop_server
786
+ }
787
+ }
788
+ ```
789
+
790
+ The result of the web service in this example is as follows.
791
+
792
+ ```
793
+ $ curl http://localhost:8000/
794
+ count: 1
795
+ $ curl http://localhost:8000/
796
+ count: 2
797
+ $ curl http://localhost:8000/
798
+ count: 3
799
+ $ ls -l *.pstore
800
+ -rw-r--r-- 1 toki toki 13 Feb 5 15:21 simple_count.pstore
801
+ ```
802
+
803
+ An example of using an undefined number of pstore is as follows. By
804
+ using `Riser::ResourceSet` and sticky process pattern, you can create
805
+ a pstore object on access by each key.
806
+
807
+ ```ruby
808
+ require 'pstore'
809
+ require 'riser'
810
+
811
+ Riser::Daemon.start_daemon(daemonize: false,
812
+ daemon_name: 'simple_key_count',
813
+ listen_address: 'localhost:8000'
814
+ ) {|server|
815
+
816
+ services = Riser::DRbServices.new(4)
817
+ services.add_sticky_process_service(:pstore,
818
+ Riser::ResourceSet.build{|builder|
819
+ builder.at_create{|key|
820
+ PStore.new("simple_key_count-#{key}.pstore", true)
821
+ }
822
+ builder.at_destroy{
823
+ # nothing to do.
824
+ }
825
+ })
826
+
827
+ server.process_num = 2
828
+ server.before_start{|server_socket|
829
+ services.start_server
830
+ }
831
+ server.at_fork{
832
+ services.detach_server
833
+ }
834
+ server.preprocess{
835
+ services.start_client
836
+ }
837
+ server.dispatch{|socket|
838
+ if (line = socket.gets) then
839
+ method, uri, _version = line.split
840
+ while (line = socket.gets)
841
+ line.strip.empty? and break
842
+ end
843
+ if (method == 'GET') then
844
+ socket << "HTTP/1.0 200 OK\r\n"
845
+ socket << "Content-Type: text/plain\r\n"
846
+ socket << "\r\n"
847
+
848
+ _path, query = uri.split('?', 2)
849
+ key = query || 'default'
850
+ services.call_service(:pstore, key) {|pstore|
851
+ pstore.transaction do
852
+ pstore[:count] ||= 0
853
+ pstore[:count] += 1
854
+ socket << 'key: ' << key << "\n"
855
+ socket << 'count: ' << pstore[:count] << "\n"
856
+ end
857
+ }
858
+ end
859
+ end
860
+ }
861
+ server.after_stop{
862
+ services.stop_server
863
+ }
864
+ }
865
+ ```
866
+
867
+ The result of the web service in this example is as follows.
868
+
869
+ ```
870
+ $ curl http://localhost:8000/
871
+ key: default
872
+ count: 1
873
+ $ curl http://localhost:8000/
874
+ key: default
875
+ count: 2
876
+ $ curl http://localhost:8000/
877
+ key: default
878
+ count: 3
879
+ $ curl http://localhost:8000/?foo
880
+ key: foo
881
+ count: 1
882
+ $ curl http://localhost:8000/?foo
883
+ key: foo
884
+ count: 2
885
+ $ curl http://localhost:8000/?foo
886
+ key: foo
887
+ count: 3
888
+ $ curl http://localhost:8000/?bar
889
+ key: bar
890
+ count: 1
891
+ $ curl http://localhost:8000/?bar
892
+ key: bar
893
+ count: 2
894
+ $ curl http://localhost:8000/?bar
895
+ key: bar
896
+ count: 3
897
+ $ ls -l *.pstore
898
+ -rw-r--r-- 1 toki toki 13 Feb 5 16:16 simple_key_count-bar.pstore
899
+ -rw-r--r-- 1 toki toki 13 Feb 5 16:15 simple_key_count-default.pstore
900
+ -rw-r--r-- 1 toki toki 13 Feb 5 16:15 simple_key_count-foo.pstore
901
+ ```
902
+
903
+ Development
904
+ -----------
905
+
906
+ After checking out the repo, run `bin/setup` to install
907
+ dependencies. You can also run `bin/console` for an interactive prompt
908
+ that will allow you to experiment.
909
+
910
+ To install this gem onto your local machine, run `bundle exec rake
911
+ install`. To release a new version, update the version number in
912
+ `version.rb`, and then run `bundle exec rake release`, which will
913
+ create a git tag for the version, push git commits and tags, and push
914
+ the `.gem` file to [rubygems.org](https://rubygems.org).
915
+
916
+ Contributing
917
+ ------------
918
+
919
+ Bug reports and pull requests are welcome on GitHub at
920
+ <https://github.com/y10k/riser>.
921
+
922
+ License
923
+ -------
924
+
925
+ The gem is available as open source under the terms of the
926
+ [MIT License](https://opensource.org/licenses/MIT).