riser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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).