File Coverage

blib/lib/Yars.pm
Criterion Covered Total %
statement 61 109 55.9
branch 10 28 35.7
condition 0 7 0.0
subroutine 15 18 83.3
pod 2 3 66.6
total 88 165 53.3


line stmt bran cond sub pod time code
1             package Yars;
2              
3 23     23   606873 use strict;
  23         58  
  23         752  
4 23     23   139 use warnings;
  23         58  
  23         703  
5 23     23   623 use 5.010.1;
  23         166  
6 23     23   145 use Mojo::Base 'Clustericious::App';
  23         54  
  23         257  
7 23     23   1602375 use Yars::Routes;
  23         95  
  23         1005  
8 23     23   722 use Yars::Tools;
  23         88  
  23         1169  
9 23     23   245 use Mojo::ByteStream qw/b/;
  23         55  
  23         1570  
10 23     23   151 use File::Path qw/mkpath/;
  23         48  
  23         1223  
11 23     23   140 use Log::Log4perl qw(:easy);
  23         43  
  23         233  
12 23     23   18251 use Number::Bytes::Human qw( format_bytes parse_bytes );
  23         49000  
  23         25891  
13              
14             # ABSTRACT: Yet Another RESTful-Archive Service
15             our $VERSION = '1.30'; # VERSION
16              
17              
18             has secret => rand;
19              
20             sub startup {
21 31     31 1 340782 my $self = shift;
22              
23             $self->hook(before_dispatch => sub {
24 1078     1078   773954 my($c) = @_;
25 1078         5443 my $stream = Mojo::IOLoop->stream($c->tx->connection);
26 1078 100       35969 return unless defined $stream;
27 867         4588 $stream->timeout(3000);
28 31         264 });
29              
30 31         360 my $max_size = 53687091200;
31              
32 31         68 my $tools;
33              
34             $self->hook(
35             after_build_tx => sub {
36             # my($tx,$app) = @_;
37 1092     1092   9583429 my ( $tx ) = @_;
38 1092         5592 $tx->req->max_message_size($max_size);
39             $tx->req->content->on(body => sub {
40 1078         930365 my $content = shift;
41 1078 100       4236 my $md5_b64 = $content->headers->header('Content-MD5') or return;
42             $content->asset->on(
43             upgrade => sub {
44             #my ( $mem, $file ) = @_;
45 148         38597 my $md5 = unpack 'H*', b($md5_b64)->b64_decode;
46 148 100       5866 my $disk = $tools->disk_for($md5) or return;
47 105         486 my $tmpdir = join '/', $disk, 'tmp';
48 105 100       2523 -d $tmpdir or do { mkpath $tmpdir; chmod 0777, $tmpdir; };
  5         805  
  5         87  
49 105 50       903 -w $tmpdir or chmod 0777, $tmpdir;
50 105         648 $_[1]->tmpdir($tmpdir);
51             }
52 188         4233 );
53             }
54 1092         26439 );
55             }
56 31         317 );
57            
58 31         562 $self->SUPER::startup(@_);
59            
60 31         316015 $tools = Yars::Tools->new($self->config);
61              
62             $self->hook(
63             before_dispatch => sub {
64 1078     1078   151647 $tools->refresh_config($self->config);
65             }
66 31         413 );
67              
68 31     5083   848 $self->helper( tools => sub { $tools } );
  5083         228650  
69              
70 31 50       1015 if(my $time = $self->config->{test_expiration}) {
71 0         0 require Clustericious::Command::stop;
72 0         0 WARN "this process will stop after $time seconds";
73             Mojo::IOLoop->timer($time => sub {
74 0     0   0 WARN "self terminating after $time seconds";
75 0         0 eval { Clustericious::Command::stop->run };
  0         0  
76 0 0       0 WARN "error in stop: $@" if $@;
77 0         0 });
78             }
79            
80 31         458 $max_size = parse_bytes($self->config->max_message_size_server(default => 53687091200));
81 31         13408 INFO "max message size = " . format_bytes($max_size) . " ($max_size)";
82             }
83              
84             sub sanity_check
85             {
86 0     0 1   my($self) = @_;
87              
88 0 0         return 0 unless $self->SUPER::sanity_check;
89              
90 0           my $sane = 1;
91            
92 0           my($url) = grep { $_ eq $self->config->url } map { $_->{url} } @{ $self->config->{servers} };
  0            
  0            
  0            
93            
94 0 0         unless(defined $url)
95             {
96 0           say "url for this server is not in the disk map";
97 0           $sane = 0;
98             }
99            
100 0           my %buckets;
101            
102 0           foreach my $server (@{ $self->config->{servers} })
  0            
103             {
104 0   0       my $name = $server->{url} // 'unknown';
105 0 0         unless($server->{url})
106             {
107 0           say "server $name has no URL";
108 0           $sane = 0;
109             }
110 0 0         if(@{ $server->{disks} } > 0)
  0            
111             {
112 0           foreach my $disk (@{ $server->{disks} })
  0            
113             {
114 0   0       my $name2 = $disk->{root} // 'unknown';
115 0 0         unless($disk->{root})
116             {
117 0           say "server $name disk $name2 has no root";
118 0           $sane = 0;
119             }
120 0 0         if(@{ $disk->{buckets} })
  0            
121             {
122 0           foreach my $bucket (@{ $disk->{buckets} })
  0            
123             {
124 0 0         if($buckets{$bucket})
125             {
126 0           say "server $name disk $name2 has duplicate bucket (also seen at $buckets{$bucket})";
127 0           $sane = 0;
128             }
129             else
130             {
131 0           $buckets{$bucket} = "server $name disk $name2";
132             }
133             }
134             }
135             else
136             {
137 0           say "server $name disk $name2 has no buckets assigned";
138 0           $sane = 0;
139             }
140             }
141             }
142             else
143             {
144 0           say "server $name has no disks";
145 0           $sane = 0;
146             }
147             }
148            
149 0           $sane;
150             }
151              
152             sub generate_config {
153 0     0 0   my $self = shift;
154              
155 0   0       my $root = $ENV{CLUSTERICIOUS_CONF_DIR} || $ENV{HOME};
156              
157             return {
158 0           dirs => [
159             [qw(etc)],
160             [qw(var log)],
161             [qw(var run)],
162             [qw(var lib yars data)],
163             ],
164             files => { 'Yars.conf' => <<'CONF', 'log4perl.conf' => <<CONF2 } };
165             ---
166             % my $root = $ENV{HOME};
167             start_mode : 'hypnotoad'
168             url : http://localhost:9001
169             hypnotoad :
170             pid_file : <%= $root %>/var/run/yars.pid
171             listen :
172             - http://localhost:9001
173             servers :
174             - url : http://localhost:9001
175             disks :
176             - root : <%= $root %>/var/lib/yars/data
177             buckets : [ <%= join ',', '0'..'9', 'a' .. 'f' %> ]
178             CONF
179             log4perl.rootLogger=TRACE, LOGFILE
180             log4perl.logger.Mojolicious=TRACE
181             log4perl.appender.LOGFILE=Log::Log4perl::Appender::File
182             log4perl.appender.LOGFILE.filename=$root/var/log/yars.log
183             log4perl.appender.LOGFILE.mode=append
184             log4perl.appender.LOGFILE.layout=PatternLayout
185             log4perl.appender.LOGFILE.layout.ConversionPattern=[%d{ISO8601}] [%7Z] %5p: %m%n
186             CONF2
187             }
188              
189              
190             1;
191              
192             __END__
193              
194             =pod
195              
196             =encoding UTF-8
197              
198             =head1 NAME
199              
200             Yars - Yet Another RESTful-Archive Service
201              
202             =head1 VERSION
203              
204             version 1.30
205              
206             =head1 SYNOPSIS
207              
208             Create a configuration in ~/etc/Yars.conf
209              
210             ---
211             url: http://localhost:9001
212             start_mode: hypnotoad
213             hypnotoad:
214             pid_file: <%= home %>/var/run/yars.pid
215             listen: [ 'http://localhost:9001' ]
216             servers:
217             - url: http://localhost:9001
218             disks:
219             - root: <%= home %>/var/data/disk1
220             buckets: <%= json [ 0..9, 'a'..'f' ] %>
221              
222             Create needed directories and run the server
223              
224             % mkdir -p ~/var/run ~/var/data/disk1
225             % yars start
226              
227             Upload a file:
228              
229             % md5sum foo.jog
230             469f9b131cce1631ddd449fbef9059ba foo.jpg
231             % yarsclient upload foo.jpg
232              
233             Download a file
234              
235             % yarsclient download foo.jpg 469f9b131cce1631ddd449fbef9059ba
236              
237             =head1 DESCRIPTION
238              
239             Yars is a simple RESTful server for data storage.
240              
241             Properly configured it provides consistent WRITE availability, and
242             eventual READ availability. Once files are written to the storage
243             cluster they are immutable (new files can -- even with the same
244             filename) can also be written to the cluster.
245              
246             It allows files to be PUT and GET based on their md5 sums and filenames,
247             and uses a distributed hash table to store the files across any number
248             of hosts and disks.
249              
250             Files are assigned to disks and hosts based on their md5s in the
251             following manner :
252              
253             The first N digits of the md5 are considered the "bucket" for a file.
254             e.g. for N=2, 256 buckets are then distributed among the disks in
255             proportion to the size of each disk. The bucket distribution is done
256             manually as part of the configuration (with the aid of an included tool,
257             L<yars_generate_diskmap>).
258              
259             The server is controlled with the command line tool L<yars>.
260              
261             The basic operations of a running yars cluster are supporting requests
262             of the form
263              
264             PUT http://$host/file/$filename
265             GET http://$host/file/$md5/$filename
266             HEAD http://$host/file/$md5/$filename
267             GET http://$host/bucket_map
268              
269             to store and retrieve files, where C<$host> may be any of the hosts in
270             the cluster, C<$md5> is the md5 of the content, and C<$filename> is a
271             filename for the content to be stored. See L<Yars::Routes> for
272             documentation of other routes.
273              
274             Failover is handled in the following manner:
275              
276             If the host to which a file is assigned is not available, then the file
277             will be "stashed" on the filesystem for the host to which it was sent.
278             If there is no space there, other hosts and disks will be tried until an
279             available one is found. Because of this failover mechanism, the "stash"
280             must be checked whenever a GET request is handled. A successful GET will
281             return quickly, but an unsuccessful one will take longer because all of
282             the stashes on all of the servers must be checked before a "404 Not
283             Found" is returned.
284              
285             Another tool L<yars_fast_balance> is provided which takes files from
286             stashes and returns them to their correct locations.
287              
288             A client L<Yars::Client> is also available (in a separate distribution),
289             for interacting with a yars server.
290              
291             =head1 EXAMPLES
292              
293             =head2 simple single server configuration
294              
295             This creates a single Yars server using hypnotoad with sixteen buckets.
296              
297             Create a configuration file in C<~/etc/Yars.conf> with this content:
298              
299             ---
300            
301             # The first half of the configuration specifies the
302             # generic Clustericious / web server settings for
303             # the server
304             start_mode : 'hypnotoad'
305             url : http://localhost:9001
306             hypnotoad :
307             pid_file : <%= home %>/var/run/yars.pid
308             listen :
309             - http://localhost:9001
310            
311             # The rest defines the servers, disks and buckets
312             # used by the Yars cluster. In this single server
313             # example, there is only one server and one disk
314             servers :
315             - url : http://localhost:9001
316             disks :
317             - root : <%= home %>/var/data/disk1
318             buckets : <%= json [ 0..9, 'a'..'f' ] %>
319              
320             The configuration file is a L<Mojo::Template> template with helpers
321             provided by L<Clustericious::Config::Helpers>.
322              
323             Create the directories needed for the server:
324              
325             % mkdir -p ~/var/run ~/var/data
326              
327             Now you can start the server process
328              
329             % yars start
330              
331             =head3 check status
332              
333             Now verify that it works:
334              
335             % curl http://localhost:9001/status
336             {"server_url":"http://localhost:9001","server_version":"1.11","app_name":"Yars","server_hostname":"iscah"}
337              
338             You can also verify that it works with L<yarsclient>:
339              
340             % yarsclient status
341             ---
342             app_name: Yars
343             server_hostname: iscah
344             server_url: http://localhost:9001
345             server_version: '1.11'
346              
347             Or via L<Yars::Client>:
348              
349             % perl -MYars::Client -MYAML::XS=Dump -E 'say Dump(Yars::Client->new->status)'
350             ---
351             app_name: Yars
352             server_hostname: iscah
353             server_url: http://localhost:9001
354             server_version: '1.11'
355              
356             =head3 upload and downloads
357              
358             Now try storing a file:
359              
360             % echo "hi" | curl -D headers.txt -T - http://localhost:9001/file/test_file1
361             ok
362             % grep Location headers.txt
363             Location: http://localhost:9001/file/764efa883dda1e11db47671c4a3bbd9e/test_file1
364              
365             You can use the Location header to fetch the file at a later time
366              
367             % curl http://localhost:9001/file/764efa883dda1e11db47671c4a3bbd9e/test_file1
368             hi
369              
370             With L<yarsclient>
371              
372             % echo "hi" > test_file2
373             % md5sum test_file2
374             764efa883dda1e11db47671c4a3bbd9e test_file2
375             % yarsclient upload test_file2
376            
377             ... some time later ...
378            
379             % yarsclient downbload test_file2 764efa883dda1e11db47671c4a3bbd9e
380              
381             You can see the HTTP requests and responses using the C<--trace> option:
382              
383             % yarsclient --trace upload test_file2
384             % yarsclient --trace download test_file2 764efa883dda1e11db47671c4a3bbd9e
385              
386             And from Perl:
387              
388             use 5.010;
389             use Yars::Client;
390             use Digest::MD5 qw( md5_hex );
391            
392             my $y = Yars::Client->new;
393            
394             # filename as first argument,
395             # reference to content as second argument
396             $y->upload("test_file3", \"hi\n");
397            
398             # you can also skip the content like this:
399             # $y->upload("test_file3");
400             # to upload content from a local file
401            
402             my $md5 = md5_hex("hi\n");
403            
404             $y->download("test_file3", $md5);
405              
406             =head2 Multiple servers
407              
408             =head3 set up the URL
409              
410             When configuring a cluster of several hosts, the C<url> value in the
411             configuration must have the correct hostname or IP address for each host
412             that the server is running on. One way to handle this would be to have
413             a configuration file for each host:
414              
415             ---
416             # ~/etc/Yars.conf on yars1
417             url: http://yars1:9001
418              
419             ---
420             # ~/etc/Yars.conf on yars2
421             url: http://yars2:9001
422              
423             A less tedious way is to use the C<hostname> or C<hostname_full> helper
424             from L<Clustericious::Config::Helpers>. This allows you to use the same
425             configuration for all servers in the cluster:
426              
427             ---
428             # works for yars1, yars2 but not for
429             # a client host
430             url: http://<%= hostname %>:9001
431              
432             =head3 abstract the webserver configuration
433              
434             If you have multiple L<Clustericious> services on the same host, or if
435             you share configurations between multiple hosts, it may be useful to use
436             the <%= extends_config %> helper and put the web server configuration in
437             a separate file. For example:
438              
439             ---
440             # ~/etc/Yars.conf
441             % my $url = "http://" . hostname . ":9001";
442             url: <%= $url %>
443             % extends_config 'hypnotoad', url => $url, name => 'yars';
444              
445             ---
446             # ~/etc/hypnotoad.conf
447             hypnotoad :
448             pid_file : <%= home %>/var/run/<%= $name %>.pid
449             listen :
450             - <%= $url %>
451              
452             Now if you were also going to use L<PlugAuth> on the same host they
453             could share the same C<hypnotoad.conf> file with different parameters:
454              
455             ---
456             # ~/etc/PlugAuth.conf
457             % my $url = "http://" . hostname . ":3001";
458             url: <%= $url %>
459             % extends_config 'hypnotoad', url => $url, name => 'plugauth';
460              
461             =head3 generate the disk map
462              
463             Given a file with a list of hosts and disks like this called diskmap.txt:
464              
465             yars1 /disk/1a
466             yars1 /disk/1b
467             yars2 /disk/2a
468             yars2 /disk/2b
469             yars3 /disk/3a
470             yars3 /disk/3b
471              
472             You can generate a disk map using the L<yars_generate_diskmap> command:
473              
474             % yars_generate_diskmap 2 diskmap.txt > ~/etc/yars_diskmap.conf
475              
476             This will generate a diskmap configuration with the buckets evenly
477             allocated to the available disks:
478              
479             ---
480             servers :
481             - url : http://yars1:9001
482             disks :
483             - root : /disk/1a
484             buckets : [ 00, 06, 0c, 12, 18, 1e, 24, 2a, 30, 36, 3c, 42, 48,
485             4e, 54, 5a, 60, 66, 6c, 72, 78, 7e, 84, 8a, 90, 96, 9c,
486             a2, a8, ae, b4, ba, c0, c6, cc, d2, d8, de, e4, ea, f0,
487             f6, fc ]
488             - root : /disk/1b
489             buckets : [ 01, 07, 0d, 13, 19, 1f, 25, 2b, 31, 37, 3d, 43, 49,
490             4f, 55, 5b, 61, 67, 6d, 73, 79, 7f, 85, 8b, 91, 97, 9d,
491             a3, a9, af, b5, bb, c1, c7, cd, d3, d9, df, e5, eb, f1,
492             f7, fd ]
493             - url : http://yars2:9001
494             disks :
495             - root : /disk/2a
496             buckets : [ 02, 08, 0e, 14, 1a, 20, 26, 2c, 32, 38, 3e, 44, 4a,
497             50, 56, 5c, 62, 68, 6e, 74, 7a, 80, 86, 8c, 92, 98, 9e,
498             a4, aa, b0, b6, bc, c2, c8, ce, d4, da, e0, e6, ec, f2,
499             f8, fe ]
500             - root : /disk/2b
501             buckets : [ 03, 09, 0f, 15, 1b, 21, 27, 2d, 33, 39, 3f, 45, 4b,
502             51, 57, 5d, 63, 69, 6f, 75, 7b, 81, 87, 8d, 93, 99, 9f,
503             a5, ab, b1, b7, bd, c3, c9, cf, d5, db, e1, e7, ed, f3,
504             f9, ff ]
505             - url : http://yars3:9001
506             disks :
507             - root : /disk/3a
508             buckets : [ 04, 0a, 10, 16, 1c, 22, 28, 2e, 34, 3a, 40, 46, 4c,
509             52, 58, 5e, 64, 6a, 70, 76, 7c, 82, 88, 8e, 94, 9a, a0,
510             a6, ac, b2, b8, be, c4, ca, d0, d6, dc, e2, e8, ee, f4,
511             fa ]
512             - root : /disk/3b
513             buckets : [ 05, 0b, 11, 17, 1d, 23, 29, 2f, 35, 3b, 41, 47, 4d,
514             53, 59, 5f, 65, 6b, 71, 77, 7d, 83, 89, 8f, 95, 9b, a1,
515             a7, ad, b3, b9, bf, c5, cb, d1, d7, dd, e3, e9, ef, f5,
516             fb ]
517              
518             which you can now extend from the Yars.conf file:
519              
520             ---
521             # ~/etc/Yars.conf
522             % my $url = "http://" . hostname . ":9001";
523             url: <%= $url %>
524             % extends_config 'hypnotoad', url => $url, name => 'yars';
525             % extends_config 'yars_diskmap';
526              
527             Also, if for whatever reason you are unable to use the C<hostname> or
528             C<hostname_full> helper in your C<Yars.conf> file, it helps to keep your
529             diskmap configuration in a separate file that can be shared between the
530             different Yars server configuration files.
531              
532             You can now run C<yars start> on each host to start the servers. L<clad>
533             may be useful for starting "yars start" on multiple hosts at once.
534              
535             =head3 client configuration
536              
537             If you are using the C<hostname> or C<hostname_full> helpers to generate
538             the URL in the serve configuration, then you won't be able to share that
539             configuration with client systems. In addition you can specify one or
540             more failover hosts for L<Yars::Client> and C<yarsclient> to use when
541             the primary is not available:
542              
543             ---
544             # ~/etc/Yars.conf on client systems
545             url: http://yars2:9001
546             failover_urls:
547             - http://yars1:9001
548              
549             =head3 randomizing the server choices
550              
551             In order to more evenly spread the load over each node in the Yars
552             cluster, you can randomize the servers that the client considers the
553             "primary" and the "failover(s)":
554              
555             ---
556             # ~/etc/Yars.conf on client systems
557             % use List::Util qw( shuffle );
558             % my @url = shuffle map { "http://yars$_:9001" } 1..3;
559             url: <%= $url[0] %>
560             failover_urls:
561             - <%= $url[1] %>
562              
563             =head2 Accelerated downloads with nginx
564              
565             One of the advantages of Clustericious is that it integrates with a
566             number of different webservers. You can do testing with hypnotoad,
567             which comes with L<Mojolicious> (and thus a prerequisite of
568             L<Clustericious> and Yars), and then deploy to production with a more
569             capable webserver, such as nginx. The integration with nginx allows for
570             handing off some of the workload to nginx; hypnotoad is good for serving
571             dynamic web applications, but nginx is better for serving static files.
572             So with this next configuration we will show you how to configure Yars
573             to handle the selection of servers and disks and hand off the actual
574             serving of the static file to nginx.
575              
576             Once again we put the nginx configuration in its own file so that we can
577             reuse it with other L<Clustericious> services.
578              
579             ---
580             # ~/etc/nginx.conf
581             start_mode:
582             - hypnotoad
583             - nginx
584            
585             # we use hypnotoad to server the dynamic part of the app
586             # and listen to the same port on localhost
587             hypnotoad:
588             listen:
589             - http://127.0.0.1:<%= $port %>
590             pid_file: <%= home %>/var/run/<%= $name %>-hypnotoad.pid
591             proxy: 1
592            
593             # and we proxy requests on the main IP address through
594             # nginx
595             nginx:
596             '-p': <%= home %>/var/run/<%= $name %>-nginx
597             '-c': <%= home %>/var/run/<%= $name %>-nginx/conf/nginx.conf
598             autogen:
599             filename: <%= home %>/var/run/<%= $name %>-nginx/conf/nginx.conf
600             content: |
601             # autogenerated file
602             events {
603             worker_connections 4096;
604             }
605             http {
606             server {
607             listen <%= hostname %>:<%= $port %>;
608             location / {
609             proxy_pass http://127.0.0.1:<%= $port %>;
610             proxy_http_version 1.1;
611             % if($name eq 'yars') {
612             # to accelerate downloads, for Yars only
613             # we set the X-Yars-Use-X-Accel header to
614             # any value. This will trigger Yars to
615             # use nginx's X-Accel-Redirect to serve
616             # actual static files back to the client.
617             proxy_set_header X-Yars-Use-X-Accel yes;
618             % }
619             }
620             % if($name eq 'yars') {
621             # we need to make the static files available
622             # to nginx. The /static prefix is to ensure
623             # that routes (future and present) do not
624             # conflict with physical files on your disk.
625             location /static/disk/ {
626             # internal makes sure that these files
627             # won't be served to external clients
628             # without going through the yars interface
629             internal;
630             alias /disk/;
631             }
632             % }
633             }
634             }
635              
636             and once again, our C<Yars.conf> file is short and sweet:
637              
638             ---
639             % my $port = 9001;
640             url: http://<%= hostname %>:<%= $port %>
641             % extends_config 'nginx', port => $port, name => 'yars';
642             % extends_config 'yars_diskmap';
643              
644             If you are storing large files in your Yars cluster the nginx default
645             maximum request size will probably not be adequate. If you see an error
646             message like this:
647              
648             % yarsclient upload large-file.iso
649             [ERROR] 2016/09/30 11:23:48 Command.pm (204) (413) Request Entity Too Large
650              
651             Then you need to set
652             L<client_max_body_size|http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>
653             appropriately.
654              
655             http {
656             server {
657             client_max_body_size 0; # zero for no max
658             ...
659              
660             =head2 Accelerate by not checking the md5 twice
661              
662             By default, Yars checks the MD5 of files before serving them to the
663             client. L<Yars::Client> and L<yarsclient> both also check the MD5 sum
664             after downloading. This saves bandwidth if automated processes attempt
665             to redownload the same file if it is corrupted on the disk of the
666             server. The chance of error is likely much higher on the network than it
667             is on the disk, and if you prefer to do the check just on the client
668             side, then you can use set the download_md5_verify to zero.
669              
670             ---
671             % my $port = 9001;
672             url: http://<%= hostname %>:<%= $port %>
673             % extends_config 'nginx', port => $port, name => 'yars';
674             % extends_config 'yars_diskmap';
675             download_md5_verify: 0
676              
677             When you download files with other clients like C<curl> or C<wget>, the
678             MD5 check will still happen on the server side. You may request this
679             check be skipped by setting the C<X-Yars-Skip-Verify> header to any
680             value.
681              
682             =head1 ACKNOWLEDGEMENT
683              
684             Thanks to Brian Duggan (BDUGGAN) for doing most of the initial work on
685             Yars, and David Golden (XDG, DAGOLDEN) for describing Yars strength as
686             "Write availability and eventual read consistency and availability".
687              
688             =head1 SEE ALSO
689              
690             =over 4
691              
692             =item L<Yars::Client>
693              
694             Perl API interface to Yars.
695              
696             =item L<yarsclient>
697              
698             Command line client interface to Yars.
699              
700             =item L<Yars::Routes>
701              
702             HTTP REST routes useable for interfacing with Yars.
703              
704             =item L<yars_exercise>
705              
706             Automated upload / download of files to Yars for performance testing.
707              
708             =item L<Clustericious>
709              
710             Yars is built on the L<Clustericious> framework, itself heavily utilizing
711             L<Mojolicious>
712              
713             =back
714              
715             =head1 AUTHOR
716              
717             Original author: Marty Brandon
718              
719             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
720              
721             Contributors:
722              
723             Brian Duggan
724              
725             Curt Tilmes
726              
727             =head1 COPYRIGHT AND LICENSE
728              
729             This software is copyright (c) 2013 by NASA GSFC.
730              
731             This is free software; you can redistribute it and/or modify it under
732             the same terms as the Perl 5 programming language system itself.
733              
734             =cut