1package CHI::Driver; 2$CHI::Driver::VERSION = '0.60'; 3use Carp; 4use CHI::CacheObject; 5use CHI::Constants qw(CHI_Max_Time); 6use CHI::Driver::Metacache; 7use CHI::Driver::Role::HasSubcaches; 8use CHI::Driver::Role::IsSizeAware; 9use CHI::Driver::Role::IsSubcache; 10use CHI::Driver::Role::Universal; 11use CHI::Serializer::Storable; 12use CHI::Serializer::JSON; 13use CHI::Util qw(parse_duration); 14use CHI::Types qw(:all); 15use Digest::MD5; 16use Encode; 17use Hash::MoreUtils qw(slice_grep); 18use Log::Any qw($log); 19use Moo; 20use MooX::Types::MooseLike::Base qw(:all); 21use Scalar::Util qw(blessed); 22use Time::Duration; 23use Time::HiRes qw(gettimeofday); 24use strict; 25use warnings; 26 27my $default_serializer = CHI::Serializer::Storable->new(); 28my $default_key_serializer = CHI::Serializer::JSON->new(); 29my $default_key_digester = Digest::MD5->new(); 30 31my @common_params; 32{ 33 my %attr = ( 34 chi_root_class => { 35 is => 'ro', 36 }, 37 compress_threshold => { 38 is => 'ro', 39 isa => Int, 40 }, 41 constructor_params => { 42 is => 'ro', 43 init_arg => undef, 44 }, 45 driver_class => { 46 is => 'ro', 47 }, 48 expires_at => { 49 is => 'rw', 50 default => sub { CHI_Max_Time }, 51 }, 52 expires_in => { 53 is => 'rw', 54 isa => Duration, 55 coerce => \&to_Duration, 56 }, 57 expires_on_backend => { 58 is => 'ro', 59 isa => Num, 60 default => sub { 0 }, 61 }, 62 expires_variance => { 63 is => 'rw', 64 isa => Num, 65 default => sub { 0 }, 66 }, 67 has_subcaches => { 68 is => 'lazy', 69 isa => Bool, 70 init_arg => undef, 71 }, 72 is_size_aware => { 73 is => 'ro', 74 isa => Bool, 75 }, 76 is_subcache => { 77 is => 'ro', 78 isa => Bool, 79 }, 80 key_digester => { 81 is => 'ro', 82 isa => Digester, 83 coerce => \&to_Digester, 84 default => sub { $default_key_digester }, 85 }, 86 key_serializer => { 87 is => 'ro', 88 isa => Serializer, 89 coerce => \&to_Serializer, 90 default => sub { $default_key_serializer }, 91 }, 92 label => { 93 is => 'rw', 94 lazy => 1, 95 builder => 1, 96 clearer => 1, 97 predicate => 1, 98 }, 99 max_build_depth => { 100 is => 'ro', 101 default => sub { 8 }, 102 }, 103 max_key_length => { 104 is => 'ro', 105 isa => Int, 106 default => sub { 1 << 31 }, 107 }, 108 metacache => { 109 is => 'lazy', 110 clearer => 1, 111 predicate => 1, 112 }, 113 namespace => { 114 is => 'ro', 115 isa => Str, 116 default => sub { 'Default' }, 117 }, 118 on_get_error => { 119 is => 'rw', 120 isa => OnError, 121 default => sub { 'log' }, 122 }, 123 on_set_error => { 124 is => 'rw', 125 isa => OnError, 126 default => sub { 'log' }, 127 }, 128 serializer => { 129 is => 'ro', 130 isa => Serializer, 131 coerce => \&to_Serializer, 132 default => sub { $default_serializer }, 133 }, 134 short_driver_name => { 135 is => 'lazy', 136 clearer => 1, 137 predicate => 1, 138 }, 139 storage => { 140 is => 'ro', 141 }, 142 ); 143 push @common_params, keys %attr; 144 for my $attr ( keys %attr ) { 145 has $attr => %{ $attr{$attr} }; 146 } 147} 148 149sub _build_has_subcaches { undef } 150 151# These methods must be implemented by subclass 152foreach my $method (qw(fetch store remove get_keys get_namespaces)) { 153 no strict 'refs'; 154 *{$method} = sub { die "method '$method' must be implemented by subclass" }; 155} 156 157# Given a hash of params, return the subset that are not in CHI's common parameters. 158# 159push @common_params, qw( 160 discard_policy 161 discard_timeout 162 l1_cache 163 max_size 164 max_size_reduction_factor 165 mirror_cache 166 parent_cache 167 subcache_type 168 subcaches 169); 170my %common_params = map { ( $_, 1 ) } @common_params; 171 172sub non_common_constructor_params { 173 my ( $class, $params ) = @_; 174 175 return { 176 map { ( $_, $params->{$_} ) } 177 grep { !$common_params{$_} } keys(%$params) 178 }; 179} 180 181sub declare_unsupported_methods { 182 my ( $class, @methods ) = @_; 183 184 foreach my $method (@methods) { 185 no strict 'refs'; 186 *{"$class\::$method"} = 187 sub { croak "method '$method' not supported by '$class'" }; 188 } 189} 190 191sub cache_object_class { 'CHI::CacheObject' } 192 193# To override time() for testing - must be writable in a dynamically scoped way from tests 194our $Test_Time; ## no critic (ProhibitPackageVars) 195our $Build_Depth = 0; ## no critic (ProhibitPackageVars) 196 197sub valid_get_options { qw(expire_if busy_lock) } 198sub valid_set_options { qw(expires_at expires_in expires_variance) } 199 200sub BUILD { 201 my ( $self, $params ) = @_; 202 203 # Ward off infinite build recursion, e.g. from circular subcache configuration. 204 # 205 local $Build_Depth = $Build_Depth + 1; 206 die "$Build_Depth levels of CHI cache creation; infinite recursion?" 207 if ( $Build_Depth > $self->max_build_depth ); 208 209 # Save off constructor params. Used to create metacache, for 210 # example. Hopefully this will not cause circular references... 211 # 212 $self->{constructor_params} = {%$params}; 213 foreach my $param (qw(l1_cache mirror_cache parent_cache)) { 214 delete( $self->{constructor_params}->{$param} ); 215 } 216 217 # If stats enabled, add ns_stats slot for keeping track of stats 218 # 219 my $stats = $self->chi_root_class->stats; 220 if ( $stats->enabled ) { 221 $self->{ns_stats} = $stats->stats_for_driver($self); 222 } 223 224 # Call BUILD_roles on any of the roles that need initialization. 225 # 226 $self->BUILD_roles($params); 227} 228 229sub BUILD_roles { 230 231 # Will be modified by roles that need it 232} 233 234sub _build_short_driver_name { 235 my ($self) = @_; 236 237 ( my $name = $self->driver_class ) =~ s/^CHI::Driver:://; 238 239 return $name; 240} 241 242sub _build_label { 243 my ($self) = @_; 244 245 return $self->short_driver_name; 246} 247 248sub _build_metacache { 249 my $self = shift; 250 251 return CHI::Driver::Metacache->new( owner_cache => $self ); 252} 253 254sub get { 255 my ( $self, $key, %params ) = @_; 256 257 croak "must specify key" unless defined($key); 258 my $ns_stats = $self->{ns_stats}; 259 my $log_is_debug = $log->is_debug; 260 my $measure_time = defined($ns_stats) || $log_is_debug; 261 my ( $start_time, $elapsed_time ); 262 263 # Fetch cache object 264 # 265 $start_time = gettimeofday() if $measure_time; 266 my $obj = eval { $params{obj} || $self->get_object($key) }; 267 $elapsed_time = ( gettimeofday() - $start_time ) * 1000 if $measure_time; 268 if ( my $error = $@ ) { 269 $ns_stats->{'get_errors'}++ if defined($ns_stats); 270 $self->_handle_get_error( $error, $key ); 271 return undef; 272 } 273 if ( !defined $obj ) { 274 $self->_record_get_stats( 'absent_misses', $elapsed_time ) 275 if defined($ns_stats); 276 $self->_log_get_result( $log, "MISS (not in cache)", 277 $key, $elapsed_time ) 278 if $log_is_debug; 279 return undef; 280 } 281 if ( defined( my $obj_ref = $params{obj_ref} ) ) { 282 $$obj_ref = $obj; 283 } 284 285 # Check if expired 286 # 287 my $is_expired = $obj->is_expired() 288 || ( defined( $params{expire_if} ) 289 && $params{expire_if}->( $obj, $self ) ); 290 if ($is_expired) { 291 $self->_record_get_stats( 'expired_misses', $elapsed_time ) 292 if defined($ns_stats); 293 $self->_log_get_result( $log, "MISS (expired)", $key, $elapsed_time ) 294 if $log_is_debug; 295 296 # If busy_lock value provided, set a new "temporary" expiration time that many 297 # seconds forward before returning undef 298 # 299 if ( defined( my $busy_lock = $params{busy_lock} ) ) { 300 my $time = $Test_Time || time(); 301 my $busy_lock_time = $time + parse_duration($busy_lock); 302 $obj->set_early_expires_at($busy_lock_time); 303 $obj->set_expires_at($busy_lock_time); 304 $self->set_object( $key, $obj ); 305 } 306 307 return undef; 308 } 309 310 $self->_record_get_stats( 'hits', $elapsed_time ) if defined($ns_stats); 311 $self->_log_get_result( $log, "HIT", $key, $elapsed_time ) if $log_is_debug; 312 return $obj->value; 313} 314 315sub _record_get_stats { 316 my ( $self, $stat, $elapsed_time ) = @_; 317 $self->{ns_stats}->{$stat}++; 318 $self->{ns_stats}->{'get_time_ms'} += $elapsed_time; 319} 320 321sub unpack_from_data { 322 my ( $self, $key, $data ) = @_; 323 324 return $self->cache_object_class->unpack_from_data( $key, $data, 325 $self->serializer ); 326} 327 328sub get_object { 329 my ( $self, $key ) = @_; 330 331 croak "must specify key" unless defined($key); 332 $key = $self->transform_key($key); 333 334 my $data = $self->fetch($key) or return undef; 335 my $obj = $self->unpack_from_data( $key, $data ); 336 return $obj; 337} 338 339sub get_expires_at { 340 my ( $self, $key ) = @_; 341 croak "must specify key" unless defined($key); 342 343 if ( my $obj = $self->get_object($key) ) { 344 return $obj->expires_at; 345 } 346 else { 347 return; 348 } 349} 350 351sub exists_and_is_expired { 352 my ( $self, $key ) = @_; 353 croak "must specify key" unless defined($key); 354 355 if ( my $obj = $self->get_object($key) ) { 356 return $obj->is_expired; 357 } 358 else { 359 return; 360 } 361} 362 363sub is_valid { 364 my ( $self, $key ) = @_; 365 croak "must specify key" unless defined($key); 366 367 if ( my $obj = $self->get_object($key) ) { 368 return !$obj->is_expired; 369 } 370 else { 371 return; 372 } 373} 374 375sub _default_set_options { 376 my $self = shift; 377 378 return { map { $_ => $self->$_() } 379 qw( expires_at expires_in expires_variance ) }; 380} 381 382sub set { 383 my $self = shift; 384 my ( $key, $value, $options ) = @_; 385 386 croak "must specify key" unless defined($key); 387 $key = $self->transform_key($key); 388 return unless defined($value); 389 390 # Fill in $options if not passed, copy if passed, and apply defaults. 391 # 392 if ( !defined($options) ) { 393 $options = $self->_default_set_options; 394 } 395 else { 396 if ( !ref($options) ) { 397 if ( $options eq 'never' ) { 398 $options = { expires_at => CHI_Max_Time }; 399 } 400 elsif ( $options eq 'now' ) { 401 $options = { expires_in => 0 }; 402 } 403 else { 404 $options = { expires_in => $options }; 405 } 406 } 407 408 # Disregard default expires_at and expires_in if either are provided 409 # 410 if ( exists( $options->{expires_at} ) 411 || exists( $options->{expires_in} ) ) 412 { 413 $options = 414 { expires_variance => $self->expires_variance, %$options }; 415 } 416 else { 417 $options = { %{ $self->_default_set_options }, %$options }; 418 } 419 } 420 421 $self->set_with_options( $key, $value, $options ); 422} 423 424sub set_with_options { 425 my ( $self, $key, $value, $options ) = @_; 426 my $ns_stats = $self->{ns_stats}; 427 my $log_is_debug = $log->is_debug; 428 my $measure_time = defined($ns_stats) || $log_is_debug; 429 my ( $start_time, $elapsed_time ); 430 431 # Determine early and final expiration times 432 # 433 my $time = $Test_Time || time(); 434 my $created_at = $time; 435 my $expires_at = 436 ( defined( $options->{expires_in} ) ) 437 ? $time + parse_duration( $options->{expires_in} ) 438 : $options->{expires_at}; 439 my $early_expires_at = 440 defined( $options->{early_expires_at} ) ? $options->{early_expires_at} 441 : ( $expires_at == CHI_Max_Time ) ? CHI_Max_Time 442 : $expires_at - 443 ( ( $expires_at - $time ) * $options->{expires_variance} ); 444 445 # Pack into data, and store 446 # 447 my $obj = 448 $self->cache_object_class->new( $key, $value, $created_at, 449 $early_expires_at, $expires_at, $self->serializer, 450 $self->compress_threshold ); 451 if ( defined( my $obj_ref = $options->{obj_ref} ) ) { 452 $$obj_ref = $obj; 453 } 454 $start_time = gettimeofday() if $measure_time; 455 if ( $self->set_object( $key, $obj ) ) { 456 $elapsed_time = ( gettimeofday() - $start_time ) * 1000 457 if $measure_time; 458 459 # Log the set 460 # 461 if ( defined($ns_stats) ) { 462 $ns_stats->{'sets'}++; 463 $ns_stats->{'set_key_size'} += length( $obj->key ); 464 $ns_stats->{'set_value_size'} += $obj->size; 465 $ns_stats->{'set_time_ms'} += $elapsed_time; 466 } 467 if ($log_is_debug) { 468 $self->_log_set_result( $log, $obj, $elapsed_time ); 469 } 470 } 471 472 return $value; 473} 474 475sub set_object { 476 my ( $self, $key, $obj ) = @_; 477 478 my $data = $obj->pack_to_data(); 479 my $expires_on_backend = $self->expires_on_backend; 480 my @expires_in = ( 481 $expires_on_backend && $obj->expires_at < CHI_Max_Time 482 ? ( ( $obj->expires_at - time ) * $expires_on_backend ) 483 : () 484 ); 485 eval { $self->store( $key, $data, @expires_in ) }; 486 if ( my $error = $@ ) { 487 $self->{ns_stats}->{'set_errors'}++ if defined( $self->{ns_stats} ); 488 $self->_handle_set_error( $error, $obj ); 489 return 0; 490 } 491 return 1; 492} 493 494sub get_keys_iterator { 495 my ($self) = @_; 496 497 my @keys = $self->get_keys(); 498 return sub { shift(@keys) }; 499} 500 501sub clear { 502 my $self = shift; 503 die "clear takes no arguments" if @_; 504 505 $self->remove_multi( [ $self->get_keys() ] ); 506} 507 508sub expire { 509 my ( $self, $key ) = @_; 510 croak "must specify key" unless defined($key); 511 512 my $time = $Test_Time || time(); 513 if ( defined( my $obj = $self->get_object($key) ) ) { 514 my $expires_at = $time - 1; 515 $obj->set_early_expires_at($expires_at); 516 $obj->set_expires_at($expires_at); 517 $self->set_object( $key, $obj ); 518 } 519} 520 521sub compute { 522 my $self = shift; 523 my $key = shift; 524 my $wantarray = wantarray(); 525 526 # Allow these in either order for backward compatibility 527 my ( $code, $options ) = 528 ( ref( $_[0] ) eq 'CODE' ) ? ( $_[0], $_[1] ) : ( $_[1], $_[0] ); 529 530 croak "must specify key and code" unless defined($key) && defined($code); 531 532 my %get_options = 533 ( ref($options) eq 'HASH' ) 534 ? slice_grep { /(?:expire_if|busy_lock)/ } $options 535 : (); 536 my $set_options = 537 ( ref($options) eq 'HASH' ) 538 ? { slice_grep { !/(?:expire_if|busy_lock)/ } $options } 539 : $options; 540 541 my $value = $self->get( $key, %get_options ); 542 if ( !defined $value ) { 543 my ( $start_time, $elapsed_time ); 544 my $ns_stats = $self->{ns_stats}; 545 $start_time = gettimeofday if defined($ns_stats); 546 $value = $wantarray ? [ $code->() ] : $code->(); 547 $elapsed_time = ( gettimeofday() - $start_time ) * 1000 548 if defined($ns_stats); 549 $self->set( $key, $value, $set_options ); 550 if ( defined($ns_stats) ) { 551 $ns_stats->{'computes'}++; 552 $ns_stats->{'compute_time_ms'} += $elapsed_time; 553 } 554 } 555 return $wantarray ? @$value : $value; 556} 557 558sub purge { 559 my ($self) = @_; 560 561 foreach my $key ( $self->get_keys() ) { 562 if ( my $obj = $self->get_object($key) ) { 563 if ( $obj->is_expired() ) { 564 $self->remove($key); 565 } 566 } 567 } 568} 569 570sub dump_as_hash { 571 my ($self) = @_; 572 573 my %hash; 574 foreach my $key ( $self->get_keys() ) { 575 if ( defined( my $value = $self->get($key) ) ) { 576 $hash{$key} = $value; 577 } 578 } 579 return \%hash; 580} 581 582sub is_empty { 583 my ($self) = @_; 584 585 return !$self->get_keys(); 586} 587 588# 589# (SEMI-) ATOMIC OPERATIONS 590# 591 592sub add { 593 my $self = shift; 594 my $key = shift; 595 596 if ( !$self->is_valid($key) ) { 597 $self->set( $key, @_ ); 598 } 599} 600 601sub append { 602 my ( $self, $key, $new ) = @_; 603 604 my $current = $self->fetch($key) or return undef; 605 $self->store( $key, $current . $new ); 606 return 1; 607} 608 609sub replace { 610 my $self = shift; 611 my $key = shift; 612 613 if ( $self->is_valid($key) ) { 614 $self->set( $key, @_ ); 615 } 616} 617 618# 619# MULTI KEY OPERATIONS 620# 621 622sub fetch_multi_hashref { 623 my ( $self, $keys ) = @_; 624 625 return { map { ( $_, $self->fetch($_) ) } @$keys }; 626} 627 628sub get_multi_hashref_objects { 629 my ( $self, $keys ) = @_; 630 my $key_data = $self->fetch_multi_hashref($keys); 631 return { 632 map { 633 my $data = $key_data->{$_}; 634 defined($data) 635 ? ( $_, $self->unpack_from_data( $_, $data ) ) 636 : ( $_, undef ) 637 } keys(%$key_data) 638 }; 639} 640 641sub get_multi_arrayref { 642 my ( $self, $keys ) = @_; 643 croak "must specify keys" unless defined($keys); 644 my $transformed_keys = [ map { $self->transform_key($_) } @$keys ]; 645 646 my $key_count = scalar(@$keys); 647 my $keyobjs = $self->get_multi_hashref_objects($transformed_keys); 648 return [ 649 map { 650 my $key = $transformed_keys->[$_]; 651 my $obj = $keyobjs->{$key}; 652 $obj ? $self->get( $key, obj => $obj ) : undef 653 } ( 0 .. $key_count - 1 ) 654 ]; 655} 656 657sub get_multi_hashref { 658 my ( $self, $keys ) = @_; 659 croak "must specify keys" unless defined($keys); 660 661 my $key_count = scalar(@$keys); 662 my $values = $self->get_multi_arrayref($keys); 663 return { map { ( $keys->[$_], $values->[$_] ) } ( 0 .. $key_count - 1 ) }; 664} 665 666sub set_multi { 667 my $self = shift; 668 $self->store_multi(@_); 669} 670 671sub store_multi { 672 my ( $self, $key_values, $set_options ) = @_; 673 croak "must specify key_values" unless defined($key_values); 674 675 while ( my ( $key, $value ) = each(%$key_values) ) { 676 $self->set( $key, $value, $set_options ); 677 } 678} 679 680sub remove_multi { 681 my ( $self, $keys ) = @_; 682 croak "must specify keys" unless defined($keys); 683 684 foreach my $key (@$keys) { 685 $self->remove($key); 686 } 687} 688 689# 690# KEY TRANSFORMATION 691# 692 693my %escapes; 694for ( 0 .. 255 ) { 695 $escapes{ chr($_) } = sprintf( "+%02x", $_ ); 696} 697my $_fail_hi = sub { 698 croak( sprintf "Can't escape multibyte character \\x{%04X}", ord( $_[0] ) ); 699}; 700 701sub transform_key { 702 my ( $self, $key ) = @_; 703 704 if ( ref($key) ) { 705 $key = $self->key_serializer->serialize($key); 706 } 707 elsif ( Encode::is_utf8($key) && $key =~ /[^\x00-\xFF]/ ) { 708 $key = $self->encode_key($key); 709 } 710 if ( length($key) > $self->max_key_length ) { 711 $key = $self->digest_key($key); 712 } 713 714 return $key; 715} 716 717sub digest_key { 718 my ( $self, $key ) = @_; 719 720 return $self->key_digester->add($key)->hexdigest; 721} 722 723sub encode_key { 724 my ( $self, $key ) = @_; 725 726 return Encode::encode( utf8 => $key ); 727} 728 729# These will be called by drivers if necessary, and in testing. By default 730# no escaping/unescaping is necessary. 731# 732sub escape_key { $_[1] } 733sub unescape_key { $_[1] } 734 735# May be used by drivers to implement escape_key/unescape_key. 736# 737sub escape_for_filename { 738 my ( $self, $key ) = @_; 739 740 $key =~ s/([^A-Za-z0-9_\=\-\~])/$escapes{$1} || $_fail_hi->($1)/ge; 741 return $key; 742} 743 744sub unescape_for_filename { 745 my ( $self, $key ) = @_; 746 747 $key =~ s/\+([0-9A-Fa-f]{2})/chr(hex($1))/eg if defined $key; 748 return $key; 749} 750 751sub is_escaped_for_filename { 752 my ( $self, $key ) = @_; 753 754 return $self->escape_for_filename( $self->unescape_for_filename($key) ) eq 755 $key; 756} 757 758# 759# LOGGING AND ERROR HANDLING 760# 761 762sub _log_get_result { 763 my $self = shift; 764 my $log = shift; 765 my $msg = shift; 766 $log->debug( sprintf( "%s: %s", $self->_describe_cache_get(@_), $msg ) ); 767} 768 769sub _log_set_result { 770 my $self = shift; 771 my $log = shift; 772 $log->debug( $self->_describe_cache_set(@_) ); 773} 774 775sub _handle_get_error { 776 my $self = shift; 777 my $error = shift; 778 my $key = $_[0]; 779 780 my $msg = 781 sprintf( "error during %s: %s", $self->_describe_cache_get(@_), $error ); 782 $self->_dispatch_error_msg( $msg, $error, $self->on_get_error(), $key ); 783} 784 785sub _handle_set_error { 786 my ( $self, $error, $obj ) = @_; 787 788 my $msg = 789 sprintf( "error during %s: %s", $self->_describe_cache_set($obj), 790 $error ); 791 $self->_dispatch_error_msg( $msg, $error, $self->on_set_error(), 792 $obj->key ); 793} 794 795sub _dispatch_error_msg { 796 my ( $self, $msg, $error, $on_error, $key ) = @_; 797 798 for ($on_error) { 799 ( ref($_) eq 'CODE' ) && do { $_->( $msg, $key, $error ) }; 800 /^log$/ 801 && do { $log->error($msg) }; 802 /^ignore$/ && do { }; 803 /^warn$/ && do { carp $msg }; 804 /^die$/ && do { croak $msg }; 805 } 806} 807 808sub _describe_cache_get { 809 my ( $self, $key, $elapsed_time ) = @_; 810 811 return 812 sprintf( "cache get for namespace='%s', key='%s', cache='%s'" 813 . ( defined($elapsed_time) ? ", time='%dms'" : "" ), 814 $self->namespace, $key, $self->label, 815 defined($elapsed_time) ? int($elapsed_time) : () ); 816} 817 818sub _describe_cache_set { 819 my ( $self, $obj, $elapsed_time ) = @_; 820 821 my $expires_str = ( 822 ( $obj->expires_at == CHI_Max_Time ) 823 ? 'never' 824 : Time::Duration::concise( 825 Time::Duration::duration_exact( 826 $obj->expires_at - $obj->created_at 827 ) 828 ) 829 ); 830 831 return 832 sprintf( 833 "cache set for namespace='%s', key='%s', size=%d, expires='%s', cache='%s'" 834 . ( defined($elapsed_time) ? ", time='%dms'" : "" ), 835 $self->namespace, $obj->key, $obj->size, $expires_str, $self->label, 836 defined($elapsed_time) ? int($elapsed_time) : () ); 837 838} 839 8401; 841 842__END__ 843 844=pod 845 846=head1 NAME 847 848CHI::Driver - Base class for all CHI drivers 849 850=head1 VERSION 851 852version 0.60 853 854=head1 DESCRIPTION 855 856This is the base class that all CHI drivers inherit from. It provides the 857methods that one calls on $cache handles, such as get() and set(). 858 859See L<CHI/METHODS> for documentation on $cache methods, and 860L<CHI::Driver::Development|CHI::Driver::Development> for documentation on 861creating new subclasses of CHI::Driver. 862 863=head1 SEE ALSO 864 865L<CHI|CHI> 866 867=head1 AUTHOR 868 869Jonathan Swartz <swartz@pobox.com> 870 871=head1 COPYRIGHT AND LICENSE 872 873This software is copyright (c) 2012 by Jonathan Swartz. 874 875This is free software; you can redistribute it and/or modify it under 876the same terms as the Perl 5 programming language system itself. 877 878=cut 879