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   27339 use strict;
  1         2  
  1         25  
4 1     1   3 use warnings;
  1         2  
  1         39  
5              
6 1     1   3 use Fcntl ':flock'; # import LOCK_* constants
  1         1  
  1         136  
7 1     1   450 use Guard;
  1         412  
  1         43  
8 1     1   387 use IPC::ScoreBoard;
  1         7708  
  1         6  
9 1     1   510 use Moo;
  1         8046  
  1         5  
10 1     1   1043 use Path::Tiny;
  1         1  
  1         38  
11 1     1   425 use namespace::clean;
  1         6632  
  1         6  
12              
13             our $VERSION = '0.03';
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             $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             =for Pod::Coverage BUILD DEMOLISH attach create mmaped_file n_slots read_slot slot_private_size slot_shared_size
200              
201             =cut
202              
203             has mmaped_file => (
204             is => 'ro',
205             required => 1
206             );
207              
208             has n_slots => (
209             is => 'ro',
210             required => 1
211             );
212              
213             has slot_shared_size => (
214             is => 'ro',
215             required => 1
216             );
217              
218             has slot_private_size => (
219             is => 'ro',
220             required => 1
221             );
222              
223             has _mode => (
224             is => 'ro',
225             required => 1
226             );
227              
228             has _score_board => (is => 'rw');
229             has _fd => (is => 'rw');
230             has _generation_idx => (is => 'rw');
231             has _last_generation => (is => 'rw');
232             has _last_idx => (
233             is => 'rw',
234             default => sub { -1 });
235              
236             sub BUILD {
237 9     9 0 92 my $self = shift;
238 9         18 my $mode = $self->_mode;
239 9 50       26 die("unknown mode '$mode'") unless $mode =~ /(slave)|(master)/;
240              
241             # construct ids (number, actually the order) for all symbols
242             # and providers. Should be sorted to guaranttee the same
243             # ids in different proccess
244             # There is an assumption, that processes, using LeaderBoard, should
245             # restarte in case of symbols/providers change.
246              
247 9         12 my $filename = $self->mmaped_file;
248 9 50 66     168 if (!(-e $filename) && ($mode eq 'slave')) {
249 0         0 die("LeaderBoard ($filename) is abandoned, cannot attach to it (file not exists)");
250             }
251              
252 9         25 my $scoreboard_path = path($filename);
253 9 100       256 $scoreboard_path->touch if !-e $filename;
254 9         190 my $fd = $scoreboard_path->filehandle('<');
255              
256 9         483 my $score_board;
257 9 100       19 if ($mode eq 'slave') {
258             # die, if slave was able to lock it, that means, that master
259             # didn't accquired the exclusive lock, i.e. no master
260 5 100       10 flock($fd, LOCK_SH | LOCK_NB)
261             && die("LeaderBoard ($filename) is abandoned, cannot attach to it (shared lock obtained)");
262 4         379 my ($sb, $nslots, $slotsize) = IPC::ScoreBoard->open($filename);
263             # just additional check, that providers/symbols information is actual
264 4         558 my $declared_size = $self->slot_shared_size + $self->slot_private_size + 2;
265 4 50       10 die("number of slots mismatch") unless $nslots == $self->n_slots;
266 4 50       9 die("slot size mismatch") unless $slotsize == $declared_size;
267 4         4 $score_board = $sb;
268             } else {
269             # die if we can't lock it, that means, another master-process
270             # already acquired it
271 4 100       10 flock($fd, LOCK_EX | LOCK_NB)
272             || die("LeaderBoard ($filename) is owned by some other process, cannot lock it exclusively");
273             # we use the addtitional fields: for spinlock and generation
274 3         340 my $declared_size = $self->slot_shared_size + $self->slot_private_size + 2;
275 3         21 $score_board = IPC::ScoreBoard->named($filename, $self->n_slots, $declared_size, 0);
276 3         761 $self->_fd($fd);
277             }
278 7         19 $self->_generation_idx($self->slot_shared_size + 1); # [spin_lock | shared_data | generation | private_data ]
279 7         13 $self->_score_board($score_board);
280 7         166 return;
281             }
282              
283             sub DEMOLISH {
284 9     9 0 3120 my $self = shift;
285             # actually we need that only for tests
286 9 100       30 if ($self->_mode eq 'master') {
287 4 100       20 flock($self->_fd, LOCK_UN) if ($self->_fd);
288             }
289 9         389 return;
290             }
291              
292             sub attach {
293 5     5 0 530 return IPC::LeaderBoard->new({
294             _mode => 'slave',
295             @_,
296             });
297             }
298              
299             sub create {
300 4     4 0 16354 return IPC::LeaderBoard->new({
301             _mode => 'master',
302             @_,
303             });
304             }
305              
306             # our use-case implies, that if we read a bit outdated data, this is OK, because
307             # the generation field will be outdated, hence, no update would occur
308             sub read_slot {
309 9     9 0 22 my ($self, $idx) = @_;
310 9 50 33     50 die("wrong index") if ($idx >= $self->n_slots) || $idx < 0;
311              
312 9         40 my @all_values = $self->_score_board->get_all($idx);
313             # drop spinlock and generation
314 9         20 my $generation = splice @all_values, $self->_generation_idx, 1;
315 9         9 splice @all_values, 0, 1;
316              
317             # record generation + index for further possible update
318 9         14 $self->_last_idx($idx);
319 9         11 $self->_last_generation($generation);
320              
321             # separate shared and private data
322 9         17 my $shared_size = $self->slot_shared_size;
323 9         20 my @shared_values = @all_values[0 .. $shared_size - 1];
324 9         20 my @private_values = @all_values[$shared_size .. $shared_size + $self->slot_private_size - 1];
325              
326 9         46 return \@shared_values, \@private_values;
327             }
328              
329             sub update {
330 5     5 1 10 my ($self, $idx, @rest) = @_;
331 5 100 66     27 my $values = (@rest && ref($rest[0]) eq 'ARRAY') ? shift(@rest) : undef;
332 5         13 my %private_values = @rest;
333 5         5 my $operation_result = 0;
334 5 50 33     24 die("wrong index") if ($idx >= $self->n_slots) || $idx < 0;
335 5 50       12 die("update for only last read index is allowed") if $idx != $self->_last_idx;
336              
337 5         11 my $sb = $self->_score_board;
338              
339             # updating shared values
340 5 100       7 if ($values) {
341 4 50       9 die("values size mismatch slot size") if @$values != $self->slot_shared_size;
342             # obtain spin-lock
343 4         24 $sb->decr($idx, 0) until $sb->incr($idx, 0) == 1;
344             # release the lock at the end of the scope
345 4     4   20 scope_guard { $sb->decr($idx, 0) };
  4         12  
346              
347             # now we hold the record, nobody else can update it.
348             # Atomically read generation value via increment it to zero.
349             # The simple $sb->get(...) cannot be used, because it does not guarantees
350             # atomicity, i.e. slot re-write is possible due to L1/L2 caches in CPU
351 4         13 my $actual_generation = $sb->incr($idx, $self->_generation_idx, 0);
352 4 100       14 if ($actual_generation == $self->_last_generation) {
353             # now we are sure, that nobody else updated the record since our last read
354             # so we can safely update it
355              
356             # +1 because the 1st field is spinlock
357 2         15 $sb->set($idx, $_ + 1, $values->[$_]) for (0 .. @$values - 1);
358             # increment the generation field
359 2         4 $sb->incr($idx, $self->_generation_idx);
360             # success
361 2         4 $operation_result = 1;
362             }
363             }
364              
365             # updating private values
366 5 100       15 if (%private_values) {
367 4         7 my $idx_delta = $self->_generation_idx + 1;
368 4         10 while (my ($private_idx, $value) = each %private_values) {
369 5 50       14 die("wrong private index") if $private_idx >= $self->slot_private_size;
370 5         16 $sb->set($idx, $private_idx + $idx_delta, $value);
371             }
372             }
373              
374 5         20 return $operation_result;
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             =cut
387              
388             1;