File Coverage

blib/lib/AnyEvent/KVStore/Etcd.pm
Criterion Covered Total %
statement 33 66 50.0
branch 2 6 33.3
condition n/a
subroutine 10 19 52.6
pod 5 5 100.0
total 50 96 52.0


line stmt bran cond sub pod time code
1             package AnyEvent::KVStore::Etcd;
2              
3 2     2   254377 use 5.010;
  2         7  
4 2     2   10 use strict;
  2         4  
  2         80  
5 2     2   8 use warnings;
  2         23  
  2         119  
6 2     2   1094 use Net::Etcd;
  2         924631  
  2         154  
7 2     2   23 use Types::Standard qw(Str Int Bool);
  2         5  
  2         19  
8 2     2   4601 use Moo;
  2         6  
  2         13  
9 2     2   980 use JSON 'decode_json';
  2         22  
  2         24  
10 2     2   428 use MIME::Base64 'decode_base64';
  2         8  
  2         2249  
11             with 'AnyEvent::KVStore::Driver';
12              
13             =head1 NAME
14              
15             AnyEvent::KVStore::Etcd - An Etcd driver for AnyEvent::KVStore
16              
17             =head1 VERSION
18              
19             Version 0.1.0
20              
21             =cut
22              
23             our $VERSION = '0.1.0';
24              
25              
26             =head1 SYNOPSIS
27              
28             use AnyEvent::KVStore;
29             $config = { host => $host, port => $port };
30             my $store = AnyEvent::KVStore->new(module => 'etcd', config => $config);
31              
32             =head1 DESCRIPTION
33              
34             AnyEvent::KVStore::Etcd is a driver for L which uses the
35             Etcd distributed key-value store as its backend. We use the Net::Etcd driver
36             for this, though there are some important limitations.
37              
38             The primary documentation for this module is in the L module
39             but there are some important limitations discussed here.
40              
41             This module can also be used directly for simplified access to an Etcd database.
42              
43             =head2 AnyEvent Loops, Callbacks, and KVStore Operations
44              
45             Net::Etcd uses L for its transport layer. It further blocks in
46             an L loop to wait for the response. For obvious reasons, this does
47             not work. So, the main key/value operations cannot be done from inside an
48             event loop. This leads to a number of possible solutions including forking and
49             running the request in another process.
50              
51             One option, though it does incur significant startup cost, is to use L
52             and move the callback from a C call to an C call. This
53             is probably the simplest approach and it works. In general you get sequential
54             ordering but this is not a hard guarantee. Another approach might be to move
55             processing into worker threads.
56              
57             =head1 ATTRIBUTES/ACCESSORS
58              
59             If accessing the module directly, the following accessors are available. These
60             are not generally needed and are mostly used internally for managing the
61             connection to the etcd server.
62              
63             These are also keys for the config hash.
64              
65             All attributes are optional.
66              
67             =head2 host Str
68              
69             This is the hostname for the etcd connection. It defaults to localhost.
70              
71             =cut
72              
73             has host => (is => 'ro', isa => Str, default => sub { 'localhost' });
74              
75             =head2 port Int
76              
77             Port for connection. It defaults to 2379.
78              
79             =cut
80              
81             has port => (is => 'ro', isa => Int, default => sub { 2379 } );
82              
83             =head2 ssl Bool default false
84              
85             whether to use SSL or not. The default is no.
86              
87             =cut
88              
89             has ssl => (is => 'ro', isa => Bool, default => 0);
90              
91             =head2 user Str
92              
93             Username for authentication. Does not authenticate if not set.
94              
95             =cut
96              
97             has user => (is => 'ro', isa => Str);
98              
99             =head2 password Str
100              
101             Password for authentication.
102              
103             =cut
104              
105              
106             has password => (is => 'ro', isa => Str);
107              
108             =head2 cnx Net::Etcd
109              
110             This is the active connection to the etcd database.
111              
112             =cut
113              
114             # $self->_slice returns a hashref with the properties requested.
115             # This relies on the fact that Moo(se) objects are blessed hashrefs.
116              
117             sub _slice {
118 0     0   0 my $self = shift;
119 0         0 my @vars = @_;
120 0         0 return { %{$self}{@vars} };
  0         0  
121             }
122              
123             sub _etcd_connect {
124 0     0   0 my $self = shift;
125 0         0 my $cnx = Net::Etcd->new($self->_slice('host', 'port', 'ssl'));
126 0 0       0 die 'Could not create new etcd connection' unless $cnx;
127 0 0       0 $cnx->auth($self->_slice('user', 'password'))->authenticate if $self->user;
128 0         0 return $cnx;
129             }
130              
131              
132             has cnx => (is => 'ro', builder => '_etcd_connect', lazy => 1);
133              
134             =head1 METHODS
135              
136             =head2 read
137              
138             Reads a value from a key and returns a JSON document payload.
139              
140             =cut
141              
142             sub read($$) {
143 0     0 1 0 my ($self, $key) = @_;
144 0         0 my $value = $self->cnx->range({key => $key })->{response}->{content};
145 0         0 $value = decode_json($value)->{kvs}->[0]->{value};
146 0         0 return decode_base64($value);
147             }
148              
149              
150              
151             =head2 exists
152              
153             Checks to see if a key exists. Here this is no less costly than read.
154              
155             =cut
156              
157             sub exists($$) {
158 0     0 1 0 my ($self, $key) = @_;
159 0         0 my $value = $self->cnx->range({key => $key })->{response}->{content};
160 0         0 $value = decode_json($value)->{kvs}->[0]->{value};
161 0         0 return defined $value;;
162             }
163              
164             =head2 list($pfx)
165              
166             Returns a list of keys
167              
168             =cut
169              
170             # adds one to the binary representation of the string for prefix searches
171             sub _add_one($){
172 5     5   302640 my ($str) = @_;
173 5 100       42 if ($str =~ /^\xff*$/){ # for empty string too
174 2         12 return "\x00";
175             }
176 3         8 my $inc = $str;
177 3         24 $inc =~ s/([^\xff])\xff*\z/ $1 =~ tr||\x01-\xff|cr /e;
  3         16  
178 3         24 return $inc;
179             }
180              
181             sub list($$) {
182 0     0 1   my ($self, $pfx) = @_;
183 0           my $value = $self->cnx->range({key => $pfx, range_end => _add_one($pfx)})->{response}->{content};
184 0           return map { decode_base64($_->{key} ) } @{decode_json($value)->{kvs}};
  0            
  0            
185             }
186              
187              
188             =head2 write($key, $value)
189              
190             Writes the key to the database and returns 1 if successful, 0 if not.
191              
192             =cut
193              
194             sub write($$$) {
195 0     0 1   my ($self, $key, $value) = @_;
196 0           return $self->cnx->put({ key => $key, value => $value })->is_success;
197             }
198              
199             =head2 watch($pfx, $callback)
200              
201             This sets up a "watch" where notifications of changed keys are passed to the
202             script. This can only really be handled inside an AnyEvent loop because the
203             changes can come from outside the program.
204              
205             The callback takes the arguments of C<($key, $value)> of the new values.
206              
207             =cut
208              
209             sub _portability_wrapper {
210 0     0     my ($sub, $result) = @_;
211 2     2   19 use Data::Dumper;
  2         6  
  2         634  
212 0           for my $e (@{decode_json($result)->{result}->{events}}){
  0            
213 0           $e = $e->{kv};
214 0           &$sub(decode_base64($e->{key}), decode_base64($e->{value}));
215             }
216             }
217              
218             sub watch($$$) {
219 0     0 1   my ($self, $pfx, $subroutine ) = @_;
220             return $self->cnx->watch({key => $pfx, range_end => _add_one($pfx)},
221 0     0     sub { my ($result) = @_; _portability_wrapper($subroutine, $result) })->create;
  0            
  0            
222             }
223              
224              
225             =head1 AUTHOR
226              
227             Chris Travers, C<< >>
228              
229             =head1 BUGS
230              
231             Please report any bugs or feature requests to C, or through
232             the web interface at L. I will be notified, and then you'll
233             automatically be notified of progress on your bug as I make changes.
234              
235              
236              
237              
238             =head1 SUPPORT
239              
240             You can find documentation for this module with the perldoc command.
241              
242             perldoc AnyEvent::KVStore::Etcd
243              
244              
245             You can also look for information at:
246              
247             =over 4
248              
249             =item * RT: CPAN's request tracker (report bugs here)
250              
251             L
252              
253             =item * CPAN Ratings
254              
255             L
256              
257             =item * Search CPAN
258              
259             L
260              
261             =back
262              
263              
264             =head1 ACKNOWLEDGEMENTS
265              
266              
267             =head1 LICENSE AND COPYRIGHT
268              
269             This software is Copyright (c) 2023 by Chris Travers.
270              
271             This is free software, licensed under:
272              
273             The (three-clause) BSD License
274              
275              
276             =cut
277              
278             1; # End of AnyEvent::KVStore::Etcd