From c865ca7f953da1c451544f9ef6de89dc7cb3cf6f Mon Sep 17 00:00:00 2001 From: Mikhail Khorkov Date: Fri, 13 Oct 2017 22:27:47 +0700 Subject: [PATCH] Prepare module for publishing --- .travis.yml | 9 ++ META6.json | 14 +++ README.md | 151 +++++++++++++++++++++++++++++++- lib/Propius.pm6 | 130 +++++++++++++++++++++++++-- t/meta-00.t | 19 ++++ t/time-based-00-simple.t | 4 +- t/time-based-01-size-eviction.t | 4 +- t/time-based-02-time-eviction.t | 4 +- 8 files changed, 326 insertions(+), 9 deletions(-) create mode 100644 .travis.yml create mode 100644 META6.json create mode 100644 t/meta-00.t diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f3dcdf9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: perl6 + +perl6: + - latest + - '2017.09' + +install: + - rakudobrew build-zef + - zef --debug --depsonly install . \ No newline at end of file diff --git a/META6.json b/META6.json new file mode 100644 index 0000000..46c04b7 --- /dev/null +++ b/META6.json @@ -0,0 +1,14 @@ +{ + "perl" : "6.*", + "name" : "Propius", + "version" : "0.1.0", + "description" : "Memory cache with loader and eviction by time.", + "author" : [ "Mikhail Khorkov" ], + "license" : "Artistic-2.0", + "provides" : { + "Propius" : "lib/Propius.pm6" + }, + "depends" : [ "TimeUnit" ], + "tags": [ "cache" ], + "source-url" : "git://github.com/atroxaper/p6-Propius.git" +} \ No newline at end of file diff --git a/README.md b/README.md index fdce8ae..013293b 100644 --- a/README.md +++ b/README.md @@ -1 +1,150 @@ -### Propius \ No newline at end of file +[![Build Status](https://travis-ci.org/atroxaper/p6-Propius.svg?branch=master)](https://travis-ci.org/atroxaper/p6-Propius) + +Propius +======= + +Memory cache with loader and eviction by time. +Inspired by [Guava's CacheLoader](https://github.com/google/guava/wiki/CachesExplained). + +Examples +-------- + use Propius; + my $cache = eviction-based-cache( + loader => { $:key ** 2 }, # calculation of the new value for key + removal-listener => { say 'removed ', $:key, ':', $:value, ' cause ', $:cause }, + # optional listener for removed values + expire-after-write => 60); # freshness time of cached values; + + $cache.get(5); # returns prodused the new value - 25; + $cache.get(5): # returns cached value - 25; + $cache.get-if-exists(6); # returns Any + $cache.put(:9key, loader => { $:key ** 3 }); # cache value for cpecified loader + $cache.get(9); # returns cached value - 729 + # ... 60 seconds later in output (in case you use the cache) + removed 5:25 cause Expired + removed 9:729 cause Expired + +Create +------ + +You can use sub `eviction-based-cache` for creation the new cache. +Arguments are: + +`:&loader! where .signature ~~ :(:$key)` - sub with signature like (:$key). +The sub will be used for producing the new values. Obligatory argument. + +`:&removal-listener where .signature ~~ :(:$key, :$value, :$cause)` - +sub with signature like (:$key, :$value, :$cause). +The sub will be called in case when value removed from the cache. +Cause is element of enum RemoveCause. + +`:$expire-after-write` - how long the cache have to store value after its last re/write + +`:$expire-after-access` - how long the cache have to store value after its last access (read or write) + +`:$time-unit` - object of TimeUnit, indicate time unit of expire-after-write/access value. +seconds by default. + +`:$ticker` - object of Ticker, witch is used for retrieve 'current' time. +Can be specified for overriding standard behaviour (current system time),for example for testing. + +`:$size` - max capacity of the cache. + +Notes +----- + +The cache can use object keys. If you want that you have to control .WITCH method if keys. + +Of course the cache is thread-save. It simply uses OO::Monitors for synchronisation. + +Available methods +----------------- + +###get(Any:D $key) +Retrieve value by key. + + my $is-primitive = $cache.get(655360001); + +If there is no value for specified key then loader with be +used to produce the new value. + +###get-if-exists(Any:D $key) +Retrieve value by key only if it exists. + + my $is-primitive = $cache.get-if-exists(900900900900990990990991); + +If there is no value for specified key then Any will be returned. + +###put(Any:D :$key, Any:D :$value) +###put(Any:D :$key, :&loader! where .signature ~~ :(:$key)) +Store a value in cache with/without specified loader. + + $cache.put(:900900900900990990990991key, :value); + $cache.put(:2key, loader => { True }); + +It will rewrite any cached value for specified key. In that case +removal-listener will be called with old value cause Replaced. + +In case of cache already reached max capacity value which has not +been used for a longest time will be removed. In that case +removal-listener will be called with old value cause Size. + +###invalidate +###invalidateAll(List:D @keys) +###invalidateAll +Mark value/values for specified/all key/keys as invalidate. + + $cache.invalidate(655360001); + $cache.invalidateAll(<1 2 3>); + $cahce.invalidateAll(); + +The value will be removed and removal-listener will be called for each +old values cause Explicit. + +###elems +Return keys and values stored in cache as Hash. + + $cache.elems(); + +###hash +Return keys and values stored in cache as Hash. + + $cache.hash(); + +This is a copy of values. Any modification of returned cache will no have +an effect on values in the store. + +###clean-up +Clean evicted values from cache. + + $cache.clean-up(); + +This method may be invoked directly by user. + +The method invoked on each write operation and ones for several read operation +if there was no write operation recently. + +It means that evicted values will be removed on just in time of its eviction. +This is done for the purpose of optimisation - is it not requires special thread +for checking an eviction. If it is issue for you then you can call it method yourself +by some scheduled Promise for example. + +Sources +------- + +[GitHub](https://github.com/atroxaper/p6-Propius) + +Author +------ + +Mikhail Khorkov + +License +------- + +See [LICENSE](LICENSE) file for the details of the license of the code in this repository. + + + + + diff --git a/lib/Propius.pm6 b/lib/Propius.pm6 index 43390f0..d9b8818 100644 --- a/lib/Propius.pm6 +++ b/lib/Propius.pm6 @@ -6,16 +6,28 @@ use OO::Monitors; use TimeUnit; use Propius::Linked; +#|[Role of time provider. +# +#That provider will use to retrieve current time in seconds. +#You can use custom implementation for testing of time-based +#caches for example.] role Ticker { + #|Getter of current time in seconds. method now( --> Int:D) { ... }; } -class DateTimeTicker does Ticker { +#|[Default implementation of Ticker. +# +#Uses current system time.] +my class DateTimeTicker does Ticker { + #|Getter of current time as system time in seconds. method now() { return DateTime.now.posix; } } +#|[Exception witch will be thrown in case provider loader +#return not defined value.] class X::Propius::LoadingFail { has $.key; method message() { @@ -23,19 +35,36 @@ class X::Propius::LoadingFail { } } -#| Amount of reads we can do without cleanup. +#|Amount of reads we can do without cleanup. my constant READS_MAX = 20; +#|[Reason of removing some element from a cache. +# +#Expired - in case a value expired; +#Explicit - in case user removed value himself; +#Replaced - in case user overwrite value himself; +#Size - in case when max capacity is reached.] enum RemoveCause ; -enum ActionType ; +#|Name of user action with data in a cache. +my enum ActionType ; -class ValueStore { +#|[Internal representation of value in cache. +# +#Contains key-value pair, times of last actions and links +#to linked chain for each actions type.] +my class ValueStore { has $.key; has $.value is rw; has Propius::Linked::Node %.nodes{ActionType}; has Int %.last-action-at{ActionType}; + #|[Constructor. + # + #:$key! - key of stored value; + #:$value! - stored value; + #:@types! - list of ActionType. Times of last actions and linked chains + #will be computed only for that actions.] multi method new(:$key!, :$value!, :@types!) { my $blessed = self.new(:$key, :$value); for @types -> $type { @@ -44,6 +73,11 @@ class ValueStore { $blessed; } + #|[Move chain link to its head. + # + #@types - list of ActionType for witch have to move; + #%chains - Hash of ActionType -> Linked::Chain - chains for each ActionType; + #$now - current time in seconds to save.] method move-to-head-for(@types, Propius::Linked::Chain %chains, Int $now) { for %!nodes.keys.grep: * ~~ any(@types) { %chains{$_}.move-to-head(%!nodes{$_}); @@ -51,16 +85,24 @@ class ValueStore { } } + #|Remove that value from all chains. method remove-nodes() { .remove() for %!nodes.values; } + #|[Return time of last action with the value. + # + #$type - ActionType for retrieving time.] method last-at(ActionType $type) { %!last-action-at{$type}; } } -monitor EvictionBasedCache { +#|[Cache with loader and eviction by time. +# +#The cache can use object keys. If you want that you have to +#control .WITCH method if keys.] +my monitor EvictionBasedCache { has &!loader; has &!removal-listener; has Any %!expire-after-sec{ActionType}; @@ -85,6 +127,10 @@ monitor EvictionBasedCache { $!reads-wo-clean = 0; } + #|[Retrieve value by key. + # + #If there is no value for specified key then loader with be + #used to produce the new value] method get(Any:D $key) { my $value = self!retrieve($key); with $value { @@ -95,11 +141,22 @@ monitor EvictionBasedCache { } } + #|[Retrieve value by key only if it exists. + # + #If there is no value for specified key then Any will be returned.] method get-if-exists(Any:D $key) { with self!retrieve($key) { .value } else { Any } } + #|[Store a value in cache. + # + #It will rewrite any cached value for specified key. In that case + #removal-listener will be called with old value cause Replaced. + # + #In case of cache already reached max capacity value which has not + #been used for a longest time will be removed. In that case + #removal-listener will be called with old value cause Size.] multi method put(Any:D :$key, Any:D :$value) { $.clean-up(); my $previous = %!store{$key}; @@ -116,26 +173,69 @@ monitor EvictionBasedCache { $move.move-to-head-for(ActionType::.values, %!chains, $!ticker.now); } + #|[Store a value in cache with specified loader. + # + #It will rewrite any cached value for specified key. In that case + #removal-listener will be called with old value cause Replaced. + # + #In case of cache already reached max capacity value which has not + #been used for a longest time will be removed. In that case + #removal-listener will be called with old value cause Size.] multi method put(Any:D :$key, :&loader! where .signature ~~ :(:$key)) { self.put(:$key, value => self!load($key, &loader)) } + #|[Mark value for specified key as invalidate. + # + #The value will be removed and removal-listener will be called with + #old value cause Explicit.] method invalidate(Any:D $key) { self!remove($key, Explicit); } + #|[Mark values for specified keys as invalidate. + # + #The values will be removed and removal-listener will be called for + #each with old values cause Explicit.] multi method invalidateAll(List:D @keys) { self.invalidate($_) for @keys; } + #|[Mark all values in cache as invalidate. + # + #The values will be removed and removal-listener will be called for + #each with old values cause Explicit.] multi method invalidateAll() { self.invalidateAll(%!store.keys); } + #|Return amount of values already stored in the cache. method elems() { %!store.elems; } + #|[Return keys and values stored in cache as Hash. + # + #This is a copy of values. Any modification of returned cache + #will no have an effect on values in the store.] + method hash() { + my %copy{Any}; + for %!store.kv -> $key, $value { + %copy{$key} = $value.value; + } + return %copy; + } + + #|[Clean evicted values from cache. + # + #This method may be invoked directly by user. + #The method invoked on each write operation and ones for several read operation + #if there was no write operation recently. + # + #It means that evicted values will be removed on just in time of its eviction. + #This is done for the purpose of optimisation - is it not requires special thread + #for checking an eviction. If it is issue for you then you can call it method yourself + #by some scheduled Promise for example.] method clean-up() { $!reads-wo-clean = 0; while $.elems >= $!size { @@ -154,6 +254,7 @@ monitor EvictionBasedCache { } } + #|Retrieve value from cache if it exists. method !retrieve($key) { my $value = %!store{$key}; with $value { @@ -167,26 +268,31 @@ monitor EvictionBasedCache { } } + #|Wrap key and value into internal representation of value (ValueStore). method !wrap-value($key, $value) { ValueStore.new: :$key, :$value, types => %!chains.keys; } + #|Compute the new value by specified loader. method !load($key, &loader) { my $value = self!invoke-with-args((:$key), &loader); fail X::Propius::LoadingFail.new(:$key) without $value; $value; } + #|Call removal-listener about removed value. method !publish($key, $value, RemoveCause $cause) { self!invoke-with-args(%(:$key, :$value, :$cause), &!removal-listener) } + #|Invoke specified sub with specified named arguments. method !invoke-with-args(%args, &sub) { my $wanted = &sub.signature.params.map( *.name.substr(1) ).Set; my %actual = %args.grep( {$wanted{$_.key}} ).hash; &sub(|%actual); } + #|Completely remove value from cache and publish an event. method !remove($key, $cause) { my $previous = %!store{$key}; with $previous { @@ -197,6 +303,20 @@ monitor EvictionBasedCache { } } +#|[Create eviction based cache. +# +#:&loader! - sub with signature like (:$key). +# The sub will be used for producing the new values. +#:&removal-listener - sub with signature like (:$key, :$value, :$cause) +# The sub will be called in case when value removed from the cache. +# $cause is element of enum RemoveCause. +#:$expire-after-write - how long the cache have to store value after its last re/write +#:$expire-after-access - how long the cache have to store value after its last access (read or write) +#:$time-unit - object of TimeUnit, indicate time unit of expire-after-write/access value. +# seconds by default. +#:$ticker - object of Ticker, witch is used for retrieve 'current' time. +# Can be specified for overriding standard behaviour (current system time), for example for testing. +#:$size - max capacity of the cache.] sub eviction-based-cache ( :&loader! where .signature ~~ :(:$key), :&removal-listener where .signature ~~ :(:$key, :$value, :$cause) = sub {}, diff --git a/t/meta-00.t b/t/meta-00.t new file mode 100644 index 0000000..8967b38 --- /dev/null +++ b/t/meta-00.t @@ -0,0 +1,19 @@ +#!/usr/bin/env perl6 + +use v6; +use Test; +use lib 'lib'; + +plan 1; + +constant AUTHOR = ?%*ENV; + +if AUTHOR { + require Test::META <&meta-ok>; + meta-ok; + done-testing; +} +else { + skip-rest "Skipping author test"; + exit; +} \ No newline at end of file diff --git a/t/time-based-00-simple.t b/t/time-based-00-simple.t index 1b957c9..bc4bb2c 100644 --- a/t/time-based-00-simple.t +++ b/t/time-based-00-simple.t @@ -5,7 +5,7 @@ use Test; use lib 'lib'; use Propius; -plan 22; +plan 23; { my $loader-call; @@ -23,6 +23,8 @@ plan 22; $cache.put(:key(7), loader => { $:key ** 3 }); is $cache.get(7), 343, 'direct loader put'; + + is $cache.hash(), %(5, 25, 6, 16, 7, 343), 'get copy of stored values'; } { diff --git a/t/time-based-01-size-eviction.t b/t/time-based-01-size-eviction.t index 5da61e9..c6705ee 100644 --- a/t/time-based-01-size-eviction.t +++ b/t/time-based-01-size-eviction.t @@ -5,7 +5,7 @@ use Test; use lib 'lib'; use Propius; -plan 7; +plan 8; my @removed; sub r-listener { push @removed, %(key => $:key, value => $:value, cause => $:cause); } @@ -40,6 +40,8 @@ sub check-listener($key, $value, $cause) { $cache.get(8); ok check-listener(7, 49, Propius::RemoveCause::Size), 'again removed the oldest accessed'; + + is $cache.hash, %(4, 16, 6, 36, 8, 64), 'retrieve values by hash method'; } done-testing; \ No newline at end of file diff --git a/t/time-based-02-time-eviction.t b/t/time-based-02-time-eviction.t index 9929b1e..72c6588 100644 --- a/t/time-based-02-time-eviction.t +++ b/t/time-based-02-time-eviction.t @@ -6,7 +6,7 @@ use TimeUnit; use lib 'lib'; use Propius; -plan 18; +plan 19; my @removed; sub r-listener { push @removed, %(key => $:key, value => $:value, cause => $:cause); } @@ -100,6 +100,8 @@ class Ticker does Propius::Ticker { is $cache.get-if-exists(5), 25, '5 is exists'; is $cache.get-if-exists(4), Any, '4 is not exists'; + + is $cache.hash, %(5, 25), 'retrieve only one value by hash method'; } done-testing; \ No newline at end of file