File Coverage

blib/lib/IPC/LeaderBoard.pm
Criterion Covered Total %
statement 87 88 98.8
branch 29 38 76.3
condition 6 12 50.0
subroutine 15 15 100.0
pod 1 6 16.6
total 138 159 86.7


line stmt bran cond sub pod time code
1             package IPC::LeaderBoard;
2              
3 1     1   27554 use strict;
  1         2  
  1         24  
4 1     1   3 use warnings;
  1         1  
  1         23  
5              
6 1     1   3 use Fcntl ':flock'; # import LOCK_* constants
  1         5  
  1         99  
7 1     1   384 use Guard;
  1         434  
  1         41  
8 1     1   490 use IPC::ScoreBoard;
  1         7216  
  1         4  
9 1     1   597 use Moo;
  1         7963  
  1         4  
10 1     1   1049 use Path::Tiny;
  1         2  
  1         38  
11 1     1   374 use namespace::clean;
  1         6385  
  1         3  
12              
13             our $VERSION = '0.02';
14              
15             =head1 NAME
16              
17             IPC::LeaderBoard - fast per-symbol online get/update information
18              
19             =head1 VERSION
20              
21             0.02
22              
23             =head1 STATUS
24              
25             =begin HTML
26              
27            

28            
29            

30              
31             =end HTML
32              
33              
34             =head1 SYNOPSIS
35              
36             use IPC::LeaderBoard;
37              
38             # in master-process
39             my $master = IPC::LeaderBoard::create(
40             n_slots => 2, # number of symbols
41             slot_shared_size => 4, # number integers per slot, concurrent access
42             slot_private_size => 2, # number integers per slot, non-concurrent access
43             mmaped_file => "/var/run/data/my.scores", # mmaped file
44             );
45             # ... initialize data here
46              
47             # in slave processes
48             my $slave = IPC::LeaderBoard::attach(
49             # exactly the same parameters as for master
50             );
51              
52             my $leader_board = $slave; # or $master, does not matter
53              
54             # get shared and private arrays of integers for the 0-th slot
55             my ($shared, $private) = $leader_board->read_slot(0);
56              
57             # update shared integers with values 1,2,3,4 and 0-th private integer
58             # with value 6
59             my $success = $leader_board->update(0, [1, 2, 3, 4], 0 => 6, 1 => 8)
60              
61             # $shared = [1, 2, 3, 4], $private = [6, 8]
62             ($shared, $private) = $leader_board->read_slot(0);
63              
64             # update just private integer with index 1 with value 2
65             $leader_board->update(0, 1 => 2);
66              
67             # update just shared values of 0-th slot
68             my $success = $leader_board->update(0, [1, 2, 3, 4]);
69              
70             =head1 DESCRIPTION
71              
72             LeaderBoard uses shared memory IPC to fast set/get integers on arbitrary row,
73             (slot) defined by it's index.
74              
75             There are the following assumptions:
76              
77             =over 2
78              
79             =item * only one master is present
80              
81             C method dies, if it founds that some other master ownes shared
82             memory (file lock is used for that).
83              
84             =item * master is launched before slaves
85              
86             C dies, if slave finds, that master-owner isn't present, or,
87             if it presents, the masters provider/symbol information isn't actual.
88             In the last case master should be restarted first.
89              
90             =item * there is no hot-deploy mechanism
91              
92             Just restart master/slaves
93              
94             =item * read slot before update it
95              
96             The vesion/generation pattern is used do detect, whether update
97             has been successfull or not. Update failure means, some other
98             C instance updated the slot; you should re-read it
99             and try uptate it again (if the update will be still actual after
100             data refresh)
101              
102             =item * no semantical difference between slave and master
103              
104             Master was introduced to lock leadear board to prevent other masters
105             connect to it and re-initialize (corrupt) data. After attach slave validates,
106             that LeaderBoard is valid (i.e. number of slots, as well as the sizes
107             of private and shared areas match to the declared).
108              
109             Hence, master can be presented only by one instance, while slaves
110             can be presented by multiple instances.
111              
112             =item * slot data organization and consistency
113              
114             A leaderboard is an array of slots of the same size:
115              
116             +------------------------------------------------------------------------+
117             | slot 1 |
118             +------------------------------------------------------------------------+
119             | slot 2 |
120             +------------------------------------------------------------------------+
121             | ... |
122             +------------------------------------------------------------------------+
123             | slot N |
124             +------------------------------------------------------------------------+
125              
126             A slot is addressed by its index.
127              
128             Each slot contains a spin-lock, a shared part, a generation field and a private part like
129              
130             It is supposed, that only leader (independent for each slot) will update the shared part,
131             while other competitors will update only own private parts, i.e.:
132              
133             | | shared part | | private part |
134             | spin | | gene- | process1 | process2 | process3 |
135             | lock | shr1 | shr2 | ... | shrN | ration | p1 | p2 | ... | pN | p1 | p2 | ... | pN | p1 | p2 | ... | pN |
136              
137             All values (shrX and pX) in the leaderboard are integer numbers. Only the current leader updates
138             the shared part, and does that in safe manner (i.e. protected by spin-lock and generation). Each process can
139             update its own private part of a slot.
140              
141             Read or write for integer values (shr1, p1, ..) read/write B is guaranteed
142             by L, which in the final, uses special CPU-instructions for that.
143              
144             The SpinLock pattern guarantees the safety of shared part update, i.e. in
145             the case of two or more concurrent write request, they will be done in
146             sequential manner.
147              
148             The Generation pattern guarantees that you update the most recent values
149             in the shared part of the slot, i.e. if some process updated shared
150             part of the slot, between slot read and update operations of the
151             current process, than, the update request of the current process
152             would fail. You have re-read the slot, and try to update it again, but
153             after re-read the update might be not required.
154              
155             Both SpinLock and Generation patterns guarantee, that you'll never
156             can made inconsistent C, or updating non-actual data.
157              
158             In the same time, you might end up with the inconsistent C
159             of the shared data: the individual values (integer) are consistent (atomic),
160             but you they might belong to the different generations. There is an assumption
161             in the C design, that it is B: would you try to update
162             the shared data, the C will fail, hence, no any harm will occur. If
163             you need to handle that, just check return value C.
164              
165             There are no any guarantees for slot private data; but it isn't needed.
166             The shared data should store information about leader, hence when a
167             new leader arrives, it updates the information; or the current leader update
168             it's information on the LeaderBoard in the appropriate slot. No data loss might
169             occur.
170              
171             When competitor (i.e. some process) updates private data, nobody else
172             can update it (i.e. you shouldn't write progam such a way, that one
173             process-competitor updates data of the other process-competitor), hence,
174             private data cannot be corrupted if used properly.
175              
176             The private data might be inconsistent on read (e.g. competitor1 reads
177             private data of competitor2, while it is half-updated by competitor2);
178             but that shoudl be B. If it is
179             significant, use shared memory for that, re-design your approach (e.g
180             use additional slots) or use some other module.
181              
182             =back
183              
184             The update process should be rather simple: C
185             and then start all together. C / C should be wrappend into
186             C (or C & friends), to repeat seveal attempts with some delay.
187              
188             The C method might fail, (i.e. it does not returns true), when it detects,
189             that somebody else already has changed an row. It is assumed that no any harm
190             in it. If needed the row can be refreshed (re-read), and the next update
191             might be successfull.
192              
193             It is assumed, that if C returs outdated data and the C decision
194             has been taken, than update will silently fail (return false), without any
195             loud exceptions; so, the next read-update cycle might be successful, but
196             probably, the updated values are already correct, so, no immediate update
197             would occur.
198              
199             =cut
200              
201             has mmaped_file => (
202             is => 'ro',
203             required => 1
204             );
205              
206             has n_slots => (
207             is => 'ro',
208             required => 1
209             );
210              
211             has slot_shared_size => (
212             is => 'ro',
213             required => 1
214             );
215              
216             has slot_private_size => (
217             is => 'ro',
218             required => 1
219             );
220              
221             has _mode => (
222             is => 'ro',
223             required => 1
224             );
225              
226             has _score_board => (is => 'rw');
227             has _fd => (is => 'rw');
228             has _generation_idx => (is => 'rw');
229             has _last_generation => (is => 'rw');
230             has _last_idx => (
231             is => 'rw',
232             default => sub { -1 });
233              
234             sub BUILD {
235 9     9 0 90 my $self = shift;
236 9         20 my $mode = $self->_mode;
237 9 50       24 die("unknown mode '$mode'") unless $mode =~ /(slave)|(master)/;
238              
239             # construct ids (number, actually the order) for all symbols
240             # and providers. Should be sorted to guaranttee the same
241             # ids in different proccess
242             # There is an assumption, that processes, using LeaderBoard, should
243             # restarte in case of symbols/providers change.
244              
245 9         13 my $filename = $self->mmaped_file;
246 9 50 66     143 if (!(-e $filename) && ($mode eq 'slave')) {
247 0         0 die("LeaderBoard ($filename) is abandoned, cannot attach to it (file not exists)");
248             }
249              
250 9         26 my $scoreboard_path = path($filename);
251 9 100       246 $scoreboard_path->touch if !-e $filename;
252 9         206 my $fd = $scoreboard_path->filehandle('<');
253              
254 9         465 my $score_board;
255 9 100       19 if ($mode eq 'slave') {
256             # die, if slave was able to lock it, that means, that master
257             # didn't accquired the exclusive lock, i.e. no master
258 5 100       11 flock($fd, LOCK_SH | LOCK_NB)
259             && die("LeaderBoard ($filename) is abandoned, cannot attach to it (shared lock obtained)");
260 4         110 my ($sb, $nslots, $slotsize, $extra) = IPC::ScoreBoard->open($filename);
261             # just additional check, that providers/symbols information is actual
262 4         526 my $declared_size = $self->slot_shared_size + $self->slot_private_size + 2;
263 4 50       10 die("number of slots mismatch") unless $nslots == $self->n_slots;
264 4 50       13 die("slot size mismatch") unless $slotsize == $declared_size;
265 4         5 $score_board = $sb;
266             } else {
267             # die if we can't lock it, that means, another master-process
268             # already acquired it
269 4 100       11 flock($fd, LOCK_EX | LOCK_NB)
270             || die("LeaderBoard ($filename) is owned by some other process, cannot lock it exclusively");
271             # we use the addtitional fields: for spinlock and generation
272 3         90 my $declared_size = $self->slot_shared_size + $self->slot_private_size + 2;
273 3         20 $score_board = IPC::ScoreBoard->named($filename, $self->n_slots, $declared_size, 0);
274 3         814 $self->_fd($fd);
275             }
276 7         21 $self->_generation_idx($self->slot_shared_size + 1); # [spin_lock | shared_data | generation | private_data ]
277 7         14 $self->_score_board($score_board);
278 7         175 return;
279             }
280              
281             sub DEMOLISH {
282 9     9 0 3235 my $self = shift;
283             # actually we need that only for tests
284 9 100       34 if ($self->_mode eq 'master') {
285 4 100       21 flock($self->_fd, LOCK_UN) if ($self->_fd);
286             }
287 9         198 return;
288             }
289              
290             sub attach {
291 5     5 0 515 return IPC::LeaderBoard->new({
292             _mode => 'slave',
293             @_,
294             });
295             }
296              
297             sub create {
298 4     4 0 15984 return IPC::LeaderBoard->new({
299             _mode => 'master',
300             @_,
301             });
302             }
303              
304             # our use-case implies, that if we read a bit outdated data, this is OK, because
305             # the generation field will be outdated, hence, no update would occur
306             sub read_slot {
307 9     9 0 24 my ($self, $idx) = @_;
308 9 50 33     53 die("wrong index") if ($idx >= $self->n_slots) || $idx < 0;
309              
310 9         41 my @all_values = $self->_score_board->get_all($idx);
311             # drop spinlock and generation
312 9         20 my $generation = splice @all_values, $self->_generation_idx, 1;
313 9         26 splice @all_values, 0, 1;
314              
315             # record generation + index for further possible update
316 9         14 $self->_last_idx($idx);
317 9         11 $self->_last_generation($generation);
318              
319             # separate shared and private data
320 9         11 my $shared_size = $self->slot_shared_size;
321 9         24 my @shared_values = @all_values[0 .. $shared_size - 1];
322 9         19 my @private_values = @all_values[$shared_size .. $shared_size + $self->slot_private_size - 1];
323              
324 9         52 return \@shared_values, \@private_values;
325             }
326              
327             sub update {
328 5     5 1 7 my ($self, $idx, @rest) = @_;
329 5 100 66     28 my $values = (@rest && ref($rest[0]) eq 'ARRAY') ? shift(@rest) : undef;
330 5         13 my %private_values = @rest;
331 5         5 my $operation_result = 0;
332 5 50 33     29 die("wrong index") if ($idx >= $self->n_slots) || $idx < 0;
333 5 50       18 die("update for only last read index is allowed") if $idx != $self->_last_idx;
334              
335 5         8 my $sb = $self->_score_board;
336              
337             # updating shared values
338 5 100       9 if ($values) {
339 4 50       11 die("values size mismatch slot size") if @$values != $self->slot_shared_size;
340             # obtain spin-lock
341 4         21 $sb->decr($idx, 0) until $sb->incr($idx, 0) == 1;
342             # release the lock at the end of the scope
343 4     4   18 scope_guard { $sb->decr($idx, 0) };
  4         13  
344              
345             # now we hold the record, nobody else can update it.
346             # Atomically read generation value via increment it to zero.
347             # The simple $sb->get(...) cannot be used, because it does not guarantees
348             # atomicity, i.e. slot re-write is possible due to L1/L2 caches in CPU
349 4         9 my $actual_generation = $sb->incr($idx, $self->_generation_idx, 0);
350 4 100       12 if ($actual_generation == $self->_last_generation) {
351             # now we are sure, that nobody else updated the record since our last read
352             # so we can safely update it
353              
354             # +1 because the 1st field is spinlock
355 2         17 $sb->set($idx, $_ + 1, $values->[$_]) for (0 .. @$values - 1);
356             # increment the generation field
357 2         6 $sb->incr($idx, $self->_generation_idx);
358             # success
359 2         6 $operation_result = 1;
360             }
361             }
362              
363             # updating private values
364 5 100       14 if (%private_values) {
365 4         8 my $idx_delta = $self->_generation_idx + 1;
366 4         13 while (my ($private_idx, $value) = each %private_values) {
367 5 50       16 die("wrong private index") if $private_idx >= $self->slot_private_size;
368 5         21 $sb->set($idx, $private_idx + $idx_delta, $value);
369             }
370             }
371              
372 5         21 return $operation_result;
373             }
374              
375              
376              
377             =head1 AUTHOR
378              
379             binary.com, C<< >>
380              
381             =head1 BUGS
382              
383             Please report any bugs or feature requests to
384             L.
385              
386             =head1 LICENSE AND COPYRIGHT
387              
388             Copyright (C) 2016 binary.com
389              
390             This program is free software; you can redistribute it and/or modify it
391             under the terms of the the Artistic License (2.0). You may obtain a
392             copy of the full license at:
393              
394             L
395              
396             Any use, modification, and distribution of the Standard or Modified
397             Versions is governed by this Artistic License. By using, modifying or
398             distributing the Package, you accept this license. Do not use, modify,
399             or distribute the Package, if you do not accept this license.
400              
401             If your Modified Version has been derived from a Modified Version made
402             by someone other than you, you are nevertheless required to ensure that
403             your Modified Version complies with the requirements of this license.
404              
405             This license does not grant you the right to use any trademark, service
406             mark, tradename, or logo of the Copyright Holder.
407              
408             This license includes the non-exclusive, worldwide, free-of-charge
409             patent license to make, have made, use, offer to sell, sell, import and
410             otherwise transfer the Package with respect to any patent claims
411             licensable by the Copyright Holder that are necessarily infringed by the
412             Package. If you institute patent litigation (including a cross-claim or
413             counterclaim) against any party alleging that the Package constitutes
414             direct or contributory patent infringement, then this Artistic License
415             to you shall terminate on the date that such litigation is filed.
416              
417             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
418             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
419             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
420             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
421             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
422             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
423             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
424             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
425              
426             =cut
427              
428             1;