├── .gitignore ├── .travis.yml ├── README.md ├── rebar.config ├── src ├── ecache.app.src ├── ecache.erl ├── ecache_reaper.erl └── ecache_server.erl └── test └── ecache_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar/* 2 | ebin/ 3 | tags 4 | .eunit/ 5 | *.beam 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | notifications: 3 | email: false 4 | otp_release: 5 | - 19.1 6 | - 18.2 7 | - 17.4 8 | - R16B03-1 9 | - R15B03 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ecache: Erlang ETS Based TTL Cache 2 | ================================== 3 | 4 | [![Build Status](https://secure.travis-ci.org/mattsta/ecache.png)](http://travis-ci.org/mattsta/ecache) 5 | 6 | ecache stores your cache items in ets. Each cache item gets its own monitoring 7 | process to auto-delete the cache item when the TTL expires. 8 | 9 | ecache has the API of [pcache](http://github.com/mattsta/pcache) but stores data in ets 10 | instead of storing data in individual processes. 11 | 12 | Usage 13 | ----- 14 | The cache server is designed to memoize a specific Module:Fun. The key in 15 | a cache is the Argument passed to Module:Fun/1. 16 | 17 | Start a cache: 18 | 19 | ```erlang 20 | CacheName = my_cache, 21 | M = database, 22 | F = get_result, 23 | Size = 16, % 16 MB cache 24 | Time = 300000, % 300,000 ms = 300s = 5 minute TTL 25 | Server = ecache_server:start_link(CacheName, M, F, Size, Time). 26 | ``` 27 | 28 | The TTL is an idle timer. When an entry is accessed, the TTL for the entry is reset. 29 | A cache with a five minute TTL expires entries when nothing touches an entry for five minutes. 30 | 31 | You can update a cached value by marking an entry dirty. You can mark an entry dirty with 32 | no arguments so it immediately gets deleted (the next request will request the data from 33 | your backing function again) or you can dirty an entry with a new value in-place. 34 | 35 | ### Example of Cache Usage 36 | 37 | ```erlang 38 | Result = ecache:get(my_cache, <<"bob">>). 39 | ecache:dirty(my_cache, <<"bob">>, <<"newvalue">>). % replace entry for <<"bob">> 40 | ecache:dirty(my_cache, <<"bob">>). % remove entry from cache 41 | ecache:empty(my_cache). % remove all entries from cache 42 | RandomValues = ecache:rand(my_cache, 12). 43 | RandomKeys = ecache:rand_keys(my_cache, 12). 44 | ``` 45 | 46 | ### Example of Caching Arbitrary M:F/1 Calls 47 | 48 | Bonus feature: Instead of creating one cache per backing function, you can also 49 | memoize any arbitrary M:F/1 call. 50 | 51 | ```erlang 52 | Result = ecache:memoize(my_cache, OtherMod, OtherFun, Arg). 53 | ecache:dirty_memoize(my_cache, OtherMod, OtherFun, Arg). % remove entry from cache 54 | ``` 55 | 56 | `ecache:memoize/4` helps us get around annoying issues of one-cache-per-mod-fun. 57 | Your root cache Mod:Fun could point to a function you never use if you only want to use 58 | `ecache:memoize/4` functionality. 59 | 60 | ### Supervisor Help 61 | 62 | Nobody likes writing supervisor entries by hand, so we provide a supervisor entry helper. 63 | This is quite useful because many production applications will have 5 to 100 individual cache pools. 64 | 65 | ```erlang 66 | SupervisorWorkerTuple = ecache:cache_sup(Name, M, F, Size). 67 | ``` 68 | 69 | For more examples, see https://github.com/mattsta/ecache/blob/master/test/ecache_tests.erl 70 | 71 | 72 | Status 73 | ------ 74 | ecache is pcache but converted to use ets instead of processes. ecache is more efficient, allows compression (because cache entries are stored in ets and ets supports compression), and is easier to understand. 75 | 76 | ### Version 1.0 (actually version 0.3.1) 77 | 78 | Initial conversion from pcache. Supports Erlang versions before 18.0 (uses `erlang:now()` for TTL math). 79 | 80 | ### Version 2.0 81 | 82 | Modern release. Provides proper stack traces if your underlying cache functions fail. Uses `os:timestamp()` instead of `now()` for TTL math. 83 | 84 | 85 | Building 86 | -------- 87 | rebar compile 88 | 89 | Testing 90 | ------- 91 | rebar eunit suite=ecache 92 | 93 | TODO 94 | ---- 95 | ### Add tests for 96 | 97 | * Other TTL variation 98 | * Reaper 99 | 100 | ### Future features 101 | 102 | * Cache pools? Cross-server awareness? 103 | * Expose per-entry TTL to external setting/updating 104 | * Expose ETS configuration to make compression and read/write optimizations settable per-cache. 105 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/ecache.app.src: -------------------------------------------------------------------------------- 1 | {application, ecache, 2 | [ 3 | {description, "ecache - Erlang ETS Based Cache"}, 4 | {vsn, "2.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications, [ 8 | kernel, 9 | stdlib 10 | ]}, 11 | {env, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/ecache.erl: -------------------------------------------------------------------------------- 1 | -module(ecache). 2 | -compile(export_all). 3 | 4 | -export([get/2, empty/1, total_size/1, stats/1, dirty/2, dirty/3, 5 | rand/2, rand_keys/2]). 6 | 7 | -export([memoize/4, dirty_memoize/4]). 8 | -define(TIMEOUT, infinity). 9 | 10 | %% =================================================================== 11 | %% Supervisory helpers 12 | %% =================================================================== 13 | 14 | cache_sup(Name, Mod, Fun, Size) -> 15 | {Name, 16 | {ecache_server, start_link, [Name, Mod, Fun, Size]}, 17 | permanent, brutal_kill, worker, [ecache_server]}. 18 | 19 | cache_ttl_sup(Name, Mod, Fun, Size, TTL) -> 20 | {Name, 21 | {ecache_server, start_link, [Name, Mod, Fun, Size, TTL]}, 22 | permanent, brutal_kill, worker, [ecache_server]}. 23 | 24 | %% =================================================================== 25 | %% Calls into ecache_server 26 | %% =================================================================== 27 | 28 | get(ServerName, Key) -> 29 | gen_server:call(ServerName, {get, Key}, ?TIMEOUT). 30 | 31 | memoize(MemoizeCacheServer, Module, Fun, Key) -> 32 | gen_server:call(MemoizeCacheServer, {generic_get, Module, Fun, Key}, 33 | ?TIMEOUT). 34 | 35 | dirty_memoize(MemoizeCacheServer, Module, Fun, Key) -> 36 | gen_server:cast(MemoizeCacheServer, {generic_dirty, Module, Fun, Key}). 37 | 38 | empty(RegisteredCacheServerName) -> 39 | gen_server:call(RegisteredCacheServerName, empty). 40 | 41 | total_size(ServerName) -> 42 | gen_server:call(ServerName, total_size). 43 | 44 | stats(ServerName) -> 45 | gen_server:call(ServerName, stats). 46 | 47 | dirty(ServerName, Key, NewData) -> 48 | gen_server:cast(ServerName, {dirty, Key, NewData}). 49 | 50 | dirty(ServerName, Key) -> 51 | gen_server:cast(ServerName, {dirty, Key}). 52 | 53 | rand(ServerName, Count) -> 54 | gen_server:call(ServerName, {rand, data, Count}). 55 | 56 | rand_keys(ServerName, Count) -> 57 | gen_server:call(ServerName, {rand, keys, Count}). 58 | -------------------------------------------------------------------------------- /src/ecache_reaper.erl: -------------------------------------------------------------------------------- 1 | -module(ecache_reaper). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start/2]). 6 | -export([start_link/1, start_link/2]). 7 | 8 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 9 | code_change/3]). 10 | 11 | -export([ecache_reaper/2]). % quiet unused function annoyance 12 | -record(reaper, {cache_size}). 13 | 14 | start_link(Name) -> 15 | start_link(Name, 8). 16 | 17 | start_link(CacheName, CacheSize) -> 18 | gen_server:start_link(?MODULE, [CacheName, CacheSize], []). 19 | 20 | start(CacheName, CacheSize) -> 21 | gen_server:start(?MODULE, [CacheName, CacheSize], []). 22 | 23 | %%%---------------------------------------------------------------------- 24 | %%% Callback functions from gen_server 25 | %%%---------------------------------------------------------------------- 26 | 27 | shrink_cache_to_size(_Name, CurrentCacheSize, CacheSize) 28 | when CurrentCacheSize =< CacheSize -> 29 | ok; 30 | shrink_cache_to_size(Name, _CurrentCacheSize, CacheSize) -> 31 | gen_server:call(Name, reap_oldest), 32 | shrink_cache_to_size(Name, ecache:total_size(Name), CacheSize). 33 | 34 | ecache_reaper(Name, CacheSize) -> 35 | % sleep for 4 seconds 36 | timer:sleep(4000), 37 | CurrentCacheSize = ecache:total_size(Name), 38 | shrink_cache_to_size(Name, CurrentCacheSize, CacheSize), 39 | ecache_reaper(Name, CacheSize). 40 | 41 | init([Name, CacheSizeBytes]) -> 42 | % ecache_reaper is started from ecache_server, but ecache_server can't finish 43 | % init'ing % until ecache_reaper:init/1 returns. 44 | % Use apply_after to make sure ecache_server exists when making calls. 45 | % Don't be clever and take this timer away. Compensates for chicken/egg prob. 46 | timer:apply_after(4000, ?MODULE, ecache_reaper, [Name, CacheSizeBytes]), 47 | State = #reaper{cache_size = CacheSizeBytes}, 48 | {ok, State}. 49 | 50 | handle_call(Arbitrary, _From, State) -> 51 | {reply, {arbitrary, Arbitrary}, State}. 52 | 53 | handle_cast(_Request, State) -> 54 | {noreply, State}. 55 | 56 | terminate(_Reason, _State) -> 57 | ok. 58 | 59 | handle_info(Info, State) -> 60 | io:format("Other info of: ~p~n", [Info]), 61 | {noreply, State}. 62 | 63 | code_change(_OldVsn, State, _Extra) -> 64 | {ok, State}. 65 | -------------------------------------------------------------------------------- /src/ecache_server.erl: -------------------------------------------------------------------------------- 1 | -module(ecache_server). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/3, start_link/4, start_link/5, start_link/6]). 6 | -export([init/1, handle_call/3, handle_cast/2, 7 | handle_info/2, terminate/2, code_change/3]). 8 | 9 | -record(cache, {name, datum_index, data_module, 10 | reaper_pid, data_accessor, cache_size, 11 | cache_policy, default_ttl}). 12 | 13 | -record(datum, {key, mgr, data, started, ttl_reaper = nil, 14 | last_active, ttl, type = mru, remaining_ttl}). 15 | 16 | % make 8 MB cache 17 | start_link(Name, Mod, Fun) -> 18 | start_link(Name, Mod, Fun, 8). 19 | 20 | % make 5 minute expiry cache 21 | start_link(Name, Mod, Fun, CacheSize) -> 22 | start_link(Name, Mod, Fun, CacheSize, 300000). 23 | 24 | % make MRU policy cache 25 | start_link(Name, Mod, Fun, CacheSize, CacheTime) -> 26 | start_link(Name, Mod, Fun, CacheSize, CacheTime, mru). 27 | 28 | start_link(Name, Mod, Fun, CacheSize, CacheTime, CachePolicy) -> 29 | gen_server:start_link({local, Name}, 30 | ?MODULE, [Name, Mod, Fun, CacheSize, CacheTime, CachePolicy], []). 31 | 32 | %%%---------------------------------------------------------------------- 33 | %%% Callback functions from gen_server 34 | %%%---------------------------------------------------------------------- 35 | 36 | init([Name, Mod, Fun, CacheSize, CacheTime, CachePolicy]) -> 37 | DatumIndex = ets:new(Name, [set, 38 | compressed, % yay compression 39 | public, % public because we spawn writers 40 | {keypos, 2}, % use Key stored in record 41 | {read_concurrency, true}]), 42 | case CacheSize of 43 | unlimited -> ReaperPid = nil, CacheSizeBytes = unlimited; 44 | _ -> CacheSizeBytes = CacheSize*1024*1024, 45 | {ok, ReaperPid} = ecache_reaper:start(Name, CacheSizeBytes), 46 | erlang:monitor(process, ReaperPid) 47 | end, 48 | 49 | State = #cache{name = Name, 50 | datum_index = DatumIndex, 51 | data_module = Mod, 52 | data_accessor = Fun, 53 | reaper_pid = ReaperPid, 54 | default_ttl = CacheTime, 55 | cache_policy = CachePolicy, 56 | cache_size = CacheSizeBytes}, 57 | {ok, State}. 58 | 59 | locate(DatumKey, #cache{datum_index = DatumIndex, data_module = DataModule, 60 | default_ttl = DefaultTTL, cache_policy = Policy, 61 | data_accessor = DataAccessor} = State) -> 62 | case fetch_data(key(DatumKey), State) of 63 | {ecache, notfound} -> Data = launch_datum(DatumKey, DatumIndex, DataModule, 64 | DataAccessor, DefaultTTL, Policy), 65 | {launched, Data}; 66 | Data -> {found, Data} 67 | end. 68 | 69 | locate_memoize(DatumKey, DatumIndex, DataModule, 70 | DataAccessor, DefaultTTL, Policy, State) -> 71 | case fetch_data(key(DataModule, DataAccessor, DatumKey), State) of 72 | {ecache, notfound} -> Data = launch_memoize_datum(DatumKey, 73 | DatumIndex, DataModule, 74 | DataAccessor, DefaultTTL, Policy), 75 | {launched, Data}; 76 | Data -> {found, Data} 77 | end. 78 | 79 | handle_call({generic_get, M, F, Key}, From, #cache{datum_index = DatumIndex, 80 | data_module = _DataModule, 81 | default_ttl = DefaultTTL, 82 | cache_policy = Policy, 83 | data_accessor = _DataAccessor} = State) -> 84 | % io:format("Requesting: ~p:~p(~p)~n", [M, F, Key]), 85 | spawn(fun() -> 86 | Reply = 87 | case locate_memoize(Key, DatumIndex, M, F, 88 | DefaultTTL, Policy, State) of 89 | {_, Data} -> Data 90 | end, 91 | gen_server:reply(From, Reply) 92 | end), 93 | {noreply, State}; 94 | 95 | handle_call({get, Key}, From, #cache{datum_index = _DatumIndex} = State) -> 96 | % io:format("Requesting: (~p)~n", [Key]), 97 | spawn(fun() -> 98 | Reply = 99 | case locate(Key, State) of 100 | {_, Data} -> Data 101 | end, 102 | gen_server:reply(From, Reply) 103 | end), 104 | {noreply, State}; 105 | 106 | % NB: total_size using ETS includes ETS overhead. An empty table still 107 | % has a size. 108 | handle_call(total_size, _From, #cache{datum_index = DatumIndex} = State) -> 109 | TableBytes = ets:info(DatumIndex, memory) * erlang:system_info(wordsize), 110 | {reply, TableBytes, State}; 111 | 112 | handle_call(stats, _From, #cache{datum_index = DatumIndex} = State) -> 113 | EtsInfo = ets:info(DatumIndex), 114 | CacheName = proplists:get_value(name, EtsInfo), 115 | DatumCount = proplists:get_value(size, EtsInfo), 116 | Bytes = proplists:get_value(memory, EtsInfo) * erlang:system_info(wordsize), 117 | Stats = [{cache_name, CacheName}, 118 | {memory_size_bytes, Bytes}, 119 | {datum_count, DatumCount}], 120 | {reply, Stats, State}; 121 | 122 | handle_call(empty, _From, #cache{datum_index = DatumIndex} = State) -> 123 | ets:delete_all_objects(DatumIndex), 124 | {reply, ok, State}; 125 | 126 | handle_call(reap_oldest, _From, #cache{datum_index = DatumIndex} = State) -> 127 | LeastActive = 128 | ets:foldl(fun(A, Acc) when A#datum.last_active < Acc -> A; 129 | (_, Acc) -> Acc 130 | end, 131 | os:timestamp(), 132 | DatumIndex), 133 | ets:delete(DatumIndex, LeastActive), 134 | {from, ok, State}; 135 | 136 | handle_call({rand, Type, Count}, From, 137 | #cache{datum_index = DatumIndex} = State) -> 138 | spawn(fun() -> 139 | AllKeys = get_all_keys(DatumIndex), 140 | Length = length(AllKeys), 141 | FoundData = 142 | case Length =< Count of 143 | true -> case Type of 144 | data -> [fetch_data(P, State) || P <- AllKeys]; 145 | keys -> [unkey(K) || K <- AllKeys] 146 | end; 147 | false -> RandomSet = [crypto:rand_uniform(1, Length) || 148 | _ <- lists:seq(1, Count)], 149 | RandomKeys = [lists:nth(Q, AllKeys) || Q <- RandomSet], 150 | case Type of 151 | data -> [fetch_data(P, State) || P <- RandomKeys]; 152 | keys -> [unkey(K) || K <- RandomKeys] 153 | end 154 | end, 155 | gen_server:reply(From, FoundData) 156 | end), 157 | {noreply, State}; 158 | 159 | handle_call(Arbitrary, _From, State) -> 160 | {reply, {arbitrary, Arbitrary}, State}. 161 | 162 | handle_cast({dirty, Id, NewData}, State) -> 163 | replace_datum(key(Id), NewData, State), 164 | {noreply, State}; 165 | 166 | handle_cast({dirty, Id}, #cache{datum_index = DatumIndex} = State) -> 167 | ets:delete(DatumIndex, key(Id)), 168 | {noreply, State}; 169 | 170 | handle_cast({generic_dirty, M, F, A}, 171 | #cache{datum_index = DatumIndex} = State) -> 172 | ets:delete(DatumIndex, key(M, F, A)), 173 | {noreply, State}. 174 | 175 | terminate(_Reason, _State) -> 176 | ok. 177 | 178 | handle_info({destroy,_DatumPid, ok}, State) -> 179 | {noreply, State}; 180 | 181 | handle_info({'DOWN', _Ref, process, ReaperPid, _Reason}, 182 | #cache{reaper_pid = ReaperPid, name = Name, cache_size = Size} = State) -> 183 | {NewReaperPid, _Mon} = ecache_reaper:start_link(Name, Size), 184 | {noreply, State#cache{reaper_pid = NewReaperPid}}; 185 | 186 | handle_info({'DOWN', _Ref, process, _Pid, _Reason}, State) -> 187 | {noreply, State}; 188 | 189 | handle_info(Info, State) -> 190 | io:format("Other info of: ~p~n", [Info]), 191 | {noreply, State}. 192 | 193 | code_change(_OldVsn, State, _Extra) -> 194 | {ok, State}. 195 | 196 | -compile({inline, [{key, 1}, {key, 3}]}). 197 | -compile({inline, [{unkey, 1}]}). 198 | % keys are tagged/boxed so you can't cross-pollute a cache when using 199 | % memoize mode versus the normal one-key-per-arg mode. 200 | % Implication: *always* add key(Key) to keys from a user. Don't pass user 201 | % created keys directly to ets. 202 | % The boxing overhead on 64 bit systems is: atom + tuple = 8 + 16 = 24 bytes 203 | % The boxing overhead on 32 bit systems is: atom + tuple = 4 + 8 = 12 bytes 204 | key(M, F, A) -> {ecache_multi, {M, F, A}}. 205 | key(Key) -> {ecache_plain, Key}. 206 | unkey({ecache_plain, Key}) -> Key; 207 | unkey({ecache_multi, {M, F, A}}) -> {M, F, A}. 208 | 209 | %% =================================================================== 210 | %% Private 211 | %% =================================================================== 212 | 213 | -compile({inline, [{create_datum, 4}]}). 214 | create_datum(DatumKey, Data, TTL, Type) -> 215 | Timestamp = os:timestamp(), 216 | #datum{key = DatumKey, data = Data, started = Timestamp, 217 | ttl = TTL, remaining_ttl = TTL, type = Type, 218 | last_active = Timestamp}. 219 | 220 | reap_after(EtsIndex, Key, LifeTTL) -> 221 | receive 222 | {update_ttl, NewTTL} -> reap_after(EtsIndex, Key, NewTTL) 223 | after 224 | LifeTTL -> ets:delete(EtsIndex, Key), 225 | exit(self(), kill) 226 | end. 227 | 228 | launch_datum_ttl_reaper(_, _, #datum{remaining_ttl = unlimited} = Datum) -> 229 | Datum; 230 | launch_datum_ttl_reaper(EtsIndex, Key, #datum{remaining_ttl = TTL} = Datum) -> 231 | Reaper = spawn_link(fun() -> reap_after(EtsIndex, Key, TTL) end), 232 | Datum#datum{ttl_reaper = Reaper}. 233 | 234 | 235 | -compile({inline, [{datum_error, 2}]}). 236 | datum_error(How, What) -> {ecache_datum_error, {How, What}}. 237 | 238 | launch_datum(Key, EtsIndex, Module, Accessor, TTL, CachePolicy) -> 239 | try Module:Accessor(Key) of 240 | CacheData -> UseKey = key(Key), 241 | Datum = create_datum(UseKey, CacheData, TTL, CachePolicy), 242 | LaunchedDatum = 243 | launch_datum_ttl_reaper(EtsIndex, UseKey, Datum), 244 | ets:insert(EtsIndex, LaunchedDatum), 245 | CacheData 246 | catch 247 | How:What -> datum_error({How, What}, erlang:get_stacktrace()) 248 | end. 249 | 250 | launch_memoize_datum(Key, EtsIndex, Module, Accessor, TTL, CachePolicy) -> 251 | try Module:Accessor(Key) of 252 | CacheData -> UseKey = key(Module, Accessor, Key), 253 | Datum = create_datum(UseKey, CacheData, TTL, CachePolicy), 254 | LaunchedDatum = 255 | launch_datum_ttl_reaper(EtsIndex, UseKey, Datum), 256 | ets:insert(EtsIndex, LaunchedDatum), 257 | CacheData 258 | catch 259 | How:What -> datum_error({How, What}, erlang:get_stacktrace()) 260 | end. 261 | 262 | 263 | -compile({inline, [{data_from_datum, 1}]}). 264 | data_from_datum(#datum{data = Data}) -> Data. 265 | 266 | -compile({inline, [{ping_reaper, 2}]}). 267 | ping_reaper(Reaper, NewTTL) when is_pid(Reaper) -> 268 | Reaper ! {update_ttl, NewTTL}; 269 | ping_reaper(_, _) -> ok. 270 | 271 | update_ttl(DatumIndex, #datum{key = Key, ttl = unlimited}) -> 272 | NewNow = {#datum.last_active, os:timestamp()}, 273 | ets:update_element(DatumIndex, Key, NewNow); 274 | update_ttl(DatumIndex, #datum{key = Key, started = Started, ttl = TTL, 275 | type = actual_time, ttl_reaper = Reaper}) -> 276 | Timestamp = os:timestamp(), 277 | % Get total time in seconds this datum has been running. Convert to ms. 278 | StartedNowDiff = timer:now_diff(Timestamp, Started) div 1000, 279 | % If we are less than the TTL, update with TTL-used (TTL in ms too) 280 | % else, we ran out of time. expire on next loop. 281 | TTLRemaining = if 282 | StartedNowDiff < TTL -> TTL - StartedNowDiff; 283 | true -> 0 284 | end, 285 | 286 | ping_reaper(Reaper, TTLRemaining), 287 | NewNowTTL = [{#datum.last_active, Timestamp}, 288 | {#datum.remaining_ttl, TTLRemaining}], 289 | ets:update_element(DatumIndex, Key, NewNowTTL); 290 | update_ttl(DatumIndex, #datum{key = Key, ttl = TTL, ttl_reaper = Reaper}) -> 291 | ping_reaper(Reaper, TTL), 292 | ets:update_element(DatumIndex, Key, {#datum.last_active, os:timestamp()}). 293 | 294 | fetch_data(Key, #cache{datum_index = DatumIndex}) when is_tuple(Key) -> 295 | case ets:lookup(DatumIndex, Key) of 296 | [Datum] -> update_ttl(DatumIndex, Datum), 297 | data_from_datum(Datum); 298 | [] -> {ecache, notfound} 299 | end. 300 | 301 | replace_datum(Key, Data, #cache{datum_index = DatumIndex}) when is_tuple(Key) -> 302 | NewDataActive = [{#datum.data, Data}, {#datum.last_active, os:timestamp()}], 303 | ets:update_element(DatumIndex, Key, NewDataActive), 304 | Data. 305 | 306 | get_all_keys(EtsIndex) -> 307 | get_all_keys(EtsIndex, ets:first(EtsIndex), []). 308 | 309 | get_all_keys(_, '$end_of_table', Accum) -> 310 | Accum; 311 | get_all_keys(EtsIndex, NextKey, Accum) -> 312 | get_all_keys(EtsIndex, ets:next(EtsIndex, NextKey), [NextKey | Accum]). 313 | 314 | %% =================================================================== 315 | %% Data Abstraction 316 | %% =================================================================== 317 | 318 | -------------------------------------------------------------------------------- /test/ecache_tests.erl: -------------------------------------------------------------------------------- 1 | -module(ecache_tests). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | -export([tester/1, memoize_tester/1]). 5 | 6 | -define(E(A, B), ?assertEqual(A, B)). 7 | -define(_E(A, B), ?_assertEqual(A, B)). 8 | 9 | ecache_setup() -> 10 | % start cache server tc (test cache) 11 | % 6 MB cache 12 | % 5 minute TTL per entry (300 seconds) 13 | {ok, Pid} = ecache_server:start_link(tc, ecache_tests, tester, 6, 3000), 14 | Pid. 15 | 16 | ecache_cleanup(Cache) -> 17 | exit(Cache, normal). 18 | 19 | tester(Key) when is_binary(Key) orelse is_list(Key) -> 20 | erlang:md5(Key). 21 | 22 | memoize_tester(Key) when is_binary(Key) orelse is_list(Key) -> 23 | erlang:crc32(Key). 24 | 25 | count(Properties) -> 26 | proplists:get_value(datum_count, Properties). 27 | 28 | empty_table_size() -> 29 | T = ets:new(ecache_test, [set, private]), 30 | S = ets:info(T, memory) * erlang:system_info(wordsize), 31 | true = ets:delete(T), 32 | S. 33 | 34 | ecache_test_() -> 35 | {setup, 36 | fun ecache_setup/0, 37 | fun ecache_cleanup/1, 38 | fun(_C) -> 39 | [ 40 | ?_E(erlang:md5("bob"), ecache:get(tc, "bob")), 41 | ?_E(erlang:md5("bob2"), ecache:get(tc, "bob2")), 42 | ?_E(ok, ecache:dirty(tc, "bob2")), 43 | ?_E(ok, ecache:dirty(tc, "bob2")), 44 | ?_E(["bob"], ecache:rand_keys(tc, 400)), 45 | ?_E([erlang:md5("bob")], ecache:rand(tc, 400)), 46 | ?_E(erlang:crc32("bob2"), 47 | ecache:memoize(tc, ?MODULE, memoize_tester, "bob2")), 48 | ?_E(ok, ecache:dirty_memoize(tc, ?MODULE, memoize_tester, "bob2")), 49 | ?_assertMatch(Size when Size >= 0 andalso Size < 1000, 50 | ecache:total_size(tc) - empty_table_size()), 51 | ?_E(1, count(ecache:stats(tc))), 52 | % now, sleep for 3.1 seconds and key "bob" will auto-expire 53 | ?_assertMatch(0, 54 | fun() -> timer:sleep(3100), count(ecache:stats(tc)) end()), 55 | % Make a new entry for TTL update testing 56 | ?_E(erlang:md5("bob7"), ecache:get(tc, "bob7")), 57 | ?_E(1, count(ecache:stats(tc))), 58 | % Wait 2 seconds and read it to reset the TTL to 3 seconds 59 | ?_E(erlang:md5("bob7"), 60 | fun() -> timer:sleep(2000), ecache:get(tc, "bob7") end()), 61 | % If the TTL doesn't reset, it would expire before the sleep returns: 62 | ?_E(1, fun() -> timer:sleep(2100), count(ecache:stats(tc)) end()), 63 | % Now wait another 1.1 sec for a total wait of 3.2s after the last reset 64 | ?_E(0, fun() -> timer:sleep(1100), count(ecache:stats(tc)) end()), 65 | % The size of an empty ets table is stable for me at 2416 bytes. 66 | ?_E(empty_table_size(), ecache:total_size(tc)), 67 | ?_E(ok, ecache:empty(tc)), 68 | ?_E(empty_table_size(), ecache:total_size(tc)) 69 | ] 70 | end 71 | }. 72 | 73 | --------------------------------------------------------------------------------