├── .github └── workflows │ ├── build.yml │ └── check.yml ├── .gitignore ├── .travis.yml ├── Emakefile ├── LICENSE ├── Makefile ├── README.md ├── erlang.mk ├── priv └── cache.benchmark ├── rebar.config ├── rebar.config.script ├── src ├── cache.app.src ├── cache.erl ├── cache.hrl ├── cache_app.erl ├── cache_benchmark.erl ├── cache_bucket.erl ├── cache_bucket_sup.erl ├── cache_heap.erl ├── cache_shards.erl ├── cache_shards_sup.erl ├── cache_sup.erl └── cache_util.erl └── test ├── cache_SUITE.erl ├── cache_heap_SUITE.erl ├── cache_shards_SUITE.erl ├── cover.spec └── tests.config /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - /refs/heads/master 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | name: otp ${{matrix.otp}} 12 | strategy: 13 | matrix: 14 | otp: [21.3, 22.3, 23.1] 15 | steps: 16 | 17 | - uses: actions/checkout@v2.0.0 18 | 19 | - uses: gleam-lang/setup-erlang@v1.1.0 20 | with: 21 | otp-version: ${{matrix.otp}} 22 | 23 | - run: make 24 | 25 | - name: Coveralls 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: ./rebar3 coveralls send 29 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - synchronize 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | name: otp ${{matrix.otp}} 12 | strategy: 13 | matrix: 14 | otp: [21.3, 22.3, 23.1] 15 | steps: 16 | 17 | - uses: actions/checkout@v2.0.0 18 | 19 | - uses: gleam-lang/setup-erlang@v1.1.0 20 | with: 21 | otp-version: ${{matrix.otp}} 22 | 23 | - run: make 24 | 25 | - name: Coveralls 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: ./rebar3 coveralls send 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.log 3 | *.aux 4 | *.beam 5 | *.dump 6 | *.tag.gz 7 | *.tgz 8 | *.lock 9 | ebin/ 10 | deps/ 11 | .eunit/ 12 | tests/ 13 | rebar3 14 | _build/ 15 | *.sublime-* 16 | *.iml 17 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | dist: trusty 3 | 4 | script: 5 | - make 6 | - ./rebar3 coveralls send 7 | 8 | otp_release: 9 | - 20.1 10 | - 20.0 11 | - 19.2 12 | - 18.3 13 | -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | {"src/*", [ 2 | report, 3 | verbose, 4 | {i, "include"}, 5 | {outdir, "ebin"}, 6 | debug_info 7 | ]}. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP = cache 2 | ORG = fogfish 3 | URI = 4 | 5 | include erlang.mk 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | Library implements segmented in-memory cache. 4 | 5 | [![Build Status](https://github.com/fogfish/cache/workflows/build/badge.svg)](https://github.com/fogfish/cache/actions/) 6 | [![Coverage Status](https://coveralls.io/repos/github/fogfish/cache/badge.svg?branch=master)](https://coveralls.io/github/fogfish/cache?branch=master) 7 | [![Hex.pm](https://img.shields.io/hexpm/v/cache.svg)](https://hex.pm/packages/cache) 8 | [![Hex Downloads](https://img.shields.io/hexpm/dt/cache.svg)](https://hex.pm/packages/cache) 9 | 10 | ## Inspiration 11 | 12 | Cache uses N disposable ETS tables instead of single one. The cache applies eviction and quota 13 | policies at segment level. The oldest ETS table is destroyed and new one is created when 14 | quota or TTL criteria are exceeded. This approach outperforms the traditional timestamp indexing techniques. 15 | 16 | The write operation always uses youngest segment. The read operation lookup key from youngest to oldest table until it is found same time key is moved to youngest segment to prolong TTL. If none of ETS table contains key then cache-miss occurs. 17 | 18 | The downside is inability to assign precise TTL per single cache entry. TTL is always approximated to nearest segment. (e.g. cache with 60 sec TTL and 10 segments has 6 sec accuracy on TTL) 19 | 20 | ## Key features 21 | 22 | * Key/value interface to read/write cached entities 23 | * Naive transform interface (accumulators, lists, binaries) to modify entities in-place 24 | * Check-and-store of put behavior 25 | * Supports asynchronous I/O to cache buckets 26 | * Sharding of cache bucket 27 | 28 | 29 | ## Getting started 30 | 31 | The latest version of the library is available at its `master` branch. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. 32 | 33 | The stable library release is available via hex packages, add the library as dependency to `rebar.config` 34 | 35 | ```erlang 36 | {deps, [ 37 | cache 38 | ]}. 39 | ``` 40 | 41 | 42 | ### Usage 43 | 44 | The library exposes public primary interface through exports of module [`cache.erl`](src/cache.erl). 45 | An experimental features are available through interface extensions. Please note that further releases of library would promote experimental features to [primary interface](src/cache.erl). 46 | * [`sharded_cache.erl`](src/cache_shards.erl) 47 | 48 | Build library and run the development console to evaluate key features 49 | 50 | ``` 51 | make && make run 52 | ``` 53 | 54 | ### spawn and configure 55 | 56 | Use `cache:start_link(...)` to spawn an new cache instance. It supports a configuration using property lists: 57 | * `type` - a type of ETS table to used as segment, default is `set`. See `ets:new/2` documentation for supported values. 58 | * `n` - number of cache segments, default is 10. 59 | * `ttl` - time to live of cached items in seconds, default is 600 seconds. It is recommended to use value multiple to `n`. The oldest cache segment is evicted every `ttl / n` seconds. 60 | * `size` - number of items to store in cache. It is recommended to use value multiple to `n`, each cache segment takes about `size / n` items. The size policy is applied only to youngest segment. 61 | * `memory` - rough number of bytes available for cache items. Each cache segment is allowed to take about `memory / n` bytes. Note: policy enforcement accounts Erlang word size. 62 | * `policy` - cache eviction policy, default is `lru`, supported values are Least Recently Used `lru`, Most Recently Used `mru`. 63 | * `check` - time in seconds to enforce cache policy. The default behavior enforces policy every `ttl / n` seconds. This timeout helps to optimize size/memory policy enforcement at high throughput system. The timeout is disabled by default. 64 | * `stats` - cache statistics handler either function/2 or `{M, F}` struct. 65 | * `heir` - the ownership of ETS segment is given away to the process during segment eviction. See `ets:give_away/3` for details. 66 | 67 | 68 | ### key/value interface 69 | 70 | The library implements traditional key/value interface through `put`, `get` and `remove` functions. The function `get` prolongs ttl of the item, use `lookup` to keep ttl untouched. 71 | 72 | ```erlang 73 | application:start(cache). 74 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 75 | 76 | ok = cache:put(my_cache, <<"my key">>, <<"my value">>). 77 | Val = cache:get(my_cache, <<"my key">>). 78 | ``` 79 | 80 | 81 | ### asynchronous i/o 82 | 83 | The library provides synchronous and asynchronous implementation of same functions. The asynchronous variant of function is annotated with `_` suffix. E.g. `get(...)` is a synchronous cache lookup operation (the process is blocked until cache returns); `get_(...)` is an asynchronous variant that delivers result of execution to mailbox. 84 | 85 | ```erlang 86 | application:start(cache). 87 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 88 | 89 | Ref = cache:get_(my_cache, <<"my key">>). 90 | receive {Ref, Val} -> Val end. 91 | ``` 92 | 93 | ### transform element 94 | 95 | The library allows to read-and-modify (modify in-place) cached element. You can `apply` any function over cached elements and returns the result of the function. The apply acts a transformer with three possible outcomes: 96 | * `undefined` (e.g. `fun(_) -> undefined end`) - no action is taken, old cache value remains; 97 | * unchanged value (e.g. `fun(X) -> X end`) - no action is taken, old cache value remains; 98 | * new value (e.g. `fun(X) -> <<"x", X/binary>> end`) - the value in cache is replaced with the result of the function. 99 | 100 | ```erlang 101 | application:start(cache). 102 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 103 | 104 | cache:put(my_cache, <<"my key">>, <<"x">>). 105 | cache:apply(my_cache, <<"my key">>, fun(X) -> <<"x", X/binary>> end). 106 | cache:get(my_cache, <<"my key">>). 107 | ``` 108 | 109 | The library implement helper functions to transform elements with `append` or `prepend`. 110 | 111 | ```erlang 112 | application:start(cache). 113 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 114 | 115 | cache:put(my_cache, <<"my key">>, <<"b">>). 116 | cache:append(my_cache, <<"my key">>, <<"c">>). 117 | cache:prepend(my_cache, <<"my key">>, <<"a">>). 118 | cache:get(my_cache, <<"my key">>). 119 | ``` 120 | 121 | ### accumulator 122 | 123 | ```erlang 124 | application:start(cache). 125 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 126 | 127 | cache:acc(my_cache, <<"my key">>, 1). 128 | cache:acc(my_cache, <<"my key">>, 1). 129 | cache:acc(my_cache, <<"my key">>, 1). 130 | ``` 131 | 132 | ### check-and-store 133 | 134 | The library implements the check-and-store semantic for put operations: 135 | * `add` store key/val only if cache does not already hold data for this key 136 | * `replace` store key/val only if cache does hold data for this key 137 | 138 | 139 | ### configuration via Erlang `sys.config` 140 | 141 | The cache instances are configurable via `sys.config`. These cache instances are supervised by application supervisor. 142 | 143 | ```erlang 144 | {cache, [ 145 | {my_cache, [{n, 10}, {ttl, 60}]} 146 | ]} 147 | ``` 148 | 149 | ### distributed environment 150 | 151 | The cache application uses standard Erlang distribution model. 152 | Please node that Erlang distribution uses single tcp/ip connection for message passing between nodes. 153 | Therefore, frequent read/write of large entries might impact on overall Erlang performance. 154 | 155 | 156 | The global cache instance is visible to all Erlang nodes in the cluster. 157 | ```erlang 158 | %% at a@example.com 159 | {ok, _} = cache:start_link({global, my_cache}, [{n, 10}, {ttl, 60}]). 160 | Val = cache:get({global, my_cache}, <<"my key">>). 161 | 162 | %% at b@example.com 163 | ok = cache:put({global, my_cache}, <<"my key">>, <<"my value">>). 164 | Val = cache:get({global, my_cache}, <<"my key">>). 165 | ``` 166 | 167 | The local cache instance is accessible for any Erlang nodes in the cluster. 168 | 169 | ```erlang 170 | %% a@example.com 171 | {ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]). 172 | Val = cache:get(my_cache, <<"my key">>). 173 | 174 | %% b@example.com 175 | ok = cache:put({my_cache, 'a@example.com'}, <<"my key">>, <<"my value">>). 176 | Val = cache:get({my_cache, 'a@example.com'}, <<"my key">>). 177 | ``` 178 | 179 | 180 | ### sharding 181 | 182 | Module `cache_shards` provides simple sharding on top of `cache`. It uses simple `hash(Key) rem NumShards` approach, and keeps `NumShards` in application environment. This feature is still **experimental**, its interface is a subject to change in further releases. 183 | 184 | ```erlang 185 | {ok, _} = cache_shards:start_link(my_cache, 8, [{n, 10}, {ttl, 60}]). 186 | ok = cache_shards:put(my_cache, key1, "Hello"). 187 | {ok,"Hello"} = cache_shards:get(my_cache, key1). 188 | ``` 189 | 190 | `sharded_cache` uses only small subset of `cache` API. But you can get shard name for your key and then use `cache` directly. 191 | ```erlang 192 | {ok, Shard} = cache_shards:get_shard(my_cache, key1) 193 | {ok, my_cache_2} 194 | cache:lookup(Shard, key1). 195 | "Hello" 196 | ``` 197 | 198 | 199 | ## How to Contribute 200 | 201 | The library is Apache 2.0 licensed and accepts contributions via GitHub pull requests. 202 | 203 | 1. Fork it 204 | 2. Create your feature branch (`git checkout -b my-new-feature`) 205 | 3. Commit your changes (`git commit -am 'Added some feature'`) 206 | 4. Push to the branch (`git push origin my-new-feature`) 207 | 5. Create new Pull Request 208 | 209 | The development requires [Erlang/OTP](http://www.erlang.org/downloads) version 19.0 or later and essential build tools. 210 | 211 | 212 | ### commit message 213 | 214 | The commit message helps us to write a good release note, speed-up review process. The message should address two question what changed and why. The project follows the template defined by chapter [Contributing to a Project](http://git-scm.com/book/ch5-2.html) of Git book. 215 | 216 | > 217 | > Short (50 chars or less) summary of changes 218 | > 219 | > More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. In some contexts, the first line is treated as the subject of an email and the rest of the text as the body. The blank line separating the summary from the body is critical (unless you omit the body entirely); tools like rebase can get confused if you run the two together. 220 | > 221 | > Further paragraphs come after blank lines. 222 | > 223 | > Bullet points are okay, too 224 | > 225 | > Typically a hyphen or asterisk is used for the bullet, preceded by a single space, with blank lines in between, but conventions vary here 226 | > 227 | 228 | ## Bugs 229 | 230 | If you detect a bug, please bring it to our attention via GitHub issues. Please make your report detailed and accurate so that we can identify and replicate the issues you experience: 231 | - specify the configuration of your environment, including which operating system you're using and the versions of your runtime environments 232 | - attach logs, screen shots and/or exceptions if possible 233 | - briefly summarize the steps you took to resolve or reproduce the problem 234 | 235 | 236 | ## Changelog 237 | 238 | * 2.3.0 - sharding of cache bucket (single node only) 239 | * 2.0.0 - various changes on asynchronous api, not compatible with version 1.x 240 | * 1.0.1 - production release 241 | 242 | ## Contributors 243 | 244 | * [Yuri Zhloba](https://github.com/yzh44yzh) 245 | * [Jose Luis Navarro](https://github.com/artefactop) 246 | * Valentin Micic 247 | 248 | 249 | ## License 250 | 251 | Copyright 2014 Dmitry Kolesnikov 252 | 253 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 254 | 255 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 256 | 257 | -------------------------------------------------------------------------------- /erlang.mk: -------------------------------------------------------------------------------- 1 | ## 2 | ## Copyright (C) 2012 Dmitry Kolesnikov 3 | ## 4 | ## This Makefile may be modified and distributed under the terms 5 | ## of the MIT license. See the LICENSE file for details. 6 | ## https://github.com/fogfish/makefile 7 | ## 8 | ## @doc 9 | ## This makefile is the wrapper of rebar to build and ship erlang software 10 | ## 11 | ## @version 1.0.12 12 | .PHONY: all compile test unit clean distclean run console mock-up mock-rm benchmark release dist 13 | 14 | APP := $(strip $(APP)) 15 | ORG := $(strip $(ORG)) 16 | URI := $(strip $(URI)) 17 | 18 | ## 19 | ## config 20 | PREFIX ?= /usr/local 21 | APP ?= $(notdir $(CURDIR)) 22 | ARCH = $(shell uname -m) 23 | PLAT ?= $(shell uname -s) 24 | VSN ?= $(shell test -z "`git status --porcelain`" && git describe --tags --long | sed -e 's/-g[0-9a-f]*//' | sed -e 's/-0//' || echo "`git describe --abbrev=0 --tags`-dev") 25 | LATEST ?= latest 26 | REL = ${APP}-${VSN} 27 | PKG = ${REL}+${ARCH}.${PLAT} 28 | TEST ?= tests 29 | COOKIE ?= nocookie 30 | DOCKER ?= fogfish/erlang-alpine 31 | IID = ${URI}${ORG}/${APP} 32 | 33 | ## required tools 34 | ## - rebar version (no spaces at end) 35 | ## - path to basho benchmark 36 | REBAR ?= 3.14.2 37 | BB = ../basho_bench 38 | 39 | 40 | ## erlang runtime configuration flags 41 | ROOT = $(shell pwd) 42 | ADDR = localhost.localdomain 43 | EFLAGS = \ 44 | -name ${APP}@${ADDR} \ 45 | -setcookie ${COOKIE} \ 46 | -pa ${ROOT}/_build/default/lib/*/ebin \ 47 | -pa ${ROOT}/_build/default/lib/*/priv \ 48 | -pa ${ROOT}/rel \ 49 | -kernel inet_dist_listen_min 32100 \ 50 | -kernel inet_dist_listen_max 32199 \ 51 | +P 1000000 \ 52 | +K true +A 160 -sbt ts 53 | 54 | 55 | ## erlang common test bootstrap 56 | BOOT_CT = \ 57 | -module(test). \ 58 | -export([run/1]). \ 59 | run(Spec) -> \ 60 | {ok, Test} = file:consult(Spec), \ 61 | case lists:keymember(node, 1, Test) of \ 62 | false -> \ 63 | erlang:halt(element(2, ct:run_test([{spec, Spec}]))); \ 64 | true -> \ 65 | ct_master:run(Spec), \ 66 | erlang:halt(0) \ 67 | end. 68 | 69 | 70 | ## 71 | BUILDER = FROM ${DOCKER}\nARG VERSION=\nRUN mkdir ${APP}\nCOPY . ${APP}/\nRUN cd ${APP} && make VSN=\x24{VERSION} && make release VSN=\x24{VERSION}\n 72 | SPAWNER = FROM ${DOCKER}\nENV VERSION=${VSN}\nRUN mkdir ${APP}\nCOPY . ${APP}/\nRUN cd ${APP} && make VSN=\x24{VERSION} && make release VSN=\x24{VERSION}\nCMD sh -c 'cd ${APP} && make console VSN=\x24{VERSION} RELX_REPLACE_OS_VARS=true ERL_NODE=${APP}'\n 73 | 74 | ## self extracting bundle archive 75 | BUNDLE_INIT = PREFIX=${PREFIX}\nREL=${PREFIX}/${REL}\nAPP=${APP}\nVSN=${VSN}\nLINE=`grep -a -n "BUNDLE:$$" $$0`\nmkdir -p $${REL}\ntail -n +$$(( $${LINE%%%%:*} + 1)) $$0 | gzip -vdc - | tar -C $${REL} -xvf - > /dev/null\n 76 | BUNDLE_FREE = exit\nBUNDLE:\n 77 | 78 | 79 | ##################################################################### 80 | ## 81 | ## build 82 | ## 83 | ##################################################################### 84 | all: rebar3 compile test 85 | 86 | compile: rebar3 87 | @./rebar3 compile 88 | 89 | 90 | ## 91 | ## execute common test and terminate node 92 | test: 93 | @./rebar3 ct --config=test/${TEST}.config --cover --verbose 94 | @./rebar3 cover 95 | 96 | # test: _build/test.beam 97 | # @mkdir -p /tmp/test/${APP} 98 | # @erl ${EFLAGS} -noshell -pa _build/ -pa test/ -run test run test/${TEST}.config 99 | # @F=`ls /tmp/test/${APP}/ct_run*/all.coverdata | tail -n 1` ;\ 100 | # cp $$F /tmp/test/${APP}/ct.coverdata 101 | # 102 | # _build/test.beam: _build/test.erl 103 | # @erlc -o _build $< 104 | # 105 | # _build/test.erl: 106 | # @mkdir -p _build && echo "${BOOT_CT}" > $@ 107 | # 108 | 109 | testclean: 110 | @rm -f _build/test.beam 111 | @rm -f _build/test.erl 112 | @rm -f test/*.beam 113 | @rm -rf test.*-temp-data 114 | @rm -rf tests 115 | 116 | ## 117 | ## execute unit test 118 | unit: all 119 | @./rebar3 skip_deps=true eunit 120 | 121 | ## 122 | ## clean 123 | clean: testclean dockerclean 124 | -@./rebar3 clean 125 | @rm -Rf _build/builder 126 | @rm -Rf _build/default/rel 127 | @rm -rf log 128 | @rm -f relx.config 129 | @rm -f *.tar.gz 130 | @rm -f *.bundle 131 | 132 | distclean: clean 133 | -@make mock-rm 134 | -@make dist-rm 135 | -@rm -Rf _build 136 | -@rm rebar3 137 | 138 | ##################################################################### 139 | ## 140 | ## debug 141 | ## 142 | ##################################################################### 143 | run: 144 | @erl ${EFLAGS} 145 | 146 | console: ${PKG}.tar.gz 147 | @_build/default/rel/${APP}/bin/${APP} foreground 148 | 149 | mock-up: test/mock/docker-compose.yml 150 | @docker-compose -f $< up 151 | 152 | mock-rm: test/mock/docker-compose.yml 153 | -@docker-compose -f $< down --rmi all -v --remove-orphans 154 | 155 | dist-up: docker-compose.yml _build/spawner 156 | @docker-compose build 157 | @docker-compose -f $< up 158 | 159 | dist-rm: docker-compose.yml 160 | -@rm -f _build/spawner 161 | -@docker-compose -f $< down --rmi all -v --remove-orphans 162 | 163 | benchmark: 164 | @echo "==> benchmark: ${TEST}" ;\ 165 | $(BB)/basho_bench -N bb@127.0.0.1 -C nocookie priv/${TEST}.benchmark ;\ 166 | $(BB)/priv/summary.r -i tests/current ;\ 167 | open tests/current/summary.png 168 | 169 | ##################################################################### 170 | ## 171 | ## release 172 | ## 173 | ##################################################################### 174 | release: ${PKG}.tar.gz 175 | 176 | ## assemble VM release 177 | ifeq (${PLAT},$(shell uname -s)) 178 | ${PKG}.tar.gz: relx.config 179 | @./rebar3 tar -n ${APP} -v ${VSN} ;\ 180 | mv _build/default/rel/${APP}/${APP}-${VSN}.tar.gz $@ ;\ 181 | echo "==> tarball: $@" 182 | 183 | relx.config: rel/relx.config.src 184 | @cat $< | sed "s/release/release, {'${APP}', \"${VSN}\"}/" > $@ 185 | else 186 | ${PKG}.tar.gz: _build/builder 187 | @docker build --file=$< --force-rm=true --build-arg="VERSION=${VSN}" --tag=build/${APP}:latest . ;\ 188 | I=`docker create build/${APP}:latest` ;\ 189 | docker cp $$I:/${APP}/$@ $@ ;\ 190 | docker rm -f $$I ;\ 191 | docker rmi build/${APP}:latest ;\ 192 | test -f $@ && echo "==> tarball: $@" 193 | 194 | _build/builder: 195 | @mkdir -p _build && echo "${BUILDER}" > $@ 196 | endif 197 | 198 | ## build docker image 199 | docker: Dockerfile 200 | git status --porcelain 201 | test -z "`git status --porcelain`" || exit -1 202 | docker build \ 203 | --build-arg APP=${APP} \ 204 | --build-arg VSN=${VSN} \ 205 | -t ${IID}:${VSN} -f $< . 206 | docker tag ${IID}:${VSN} ${IID}:${LATEST} 207 | 208 | dockerclean: 209 | -@docker rmi -f ${IID}:${LATEST} 210 | -@docker rmi -f ${IID}:${VSN} 211 | 212 | _build/spawner: 213 | @mkdir -p _build && echo "${SPAWNER}" > $@ 214 | 215 | 216 | dist: ${PKG}.tar.gz ${PKG}.bundle 217 | 218 | 219 | ${PKG}.bundle: rel/bootstrap.sh 220 | @printf '${BUNDLE_INIT}' > $@ ;\ 221 | cat $< >> $@ ;\ 222 | printf '${BUNDLE_FREE}' >> $@ ;\ 223 | cat ${PKG}.tar.gz >> $@ ;\ 224 | chmod ugo+x $@ ;\ 225 | echo "==> bundle: $@" 226 | 227 | 228 | ##################################################################### 229 | ## 230 | ## dependencies 231 | ## 232 | ##################################################################### 233 | rebar3: 234 | @echo "==> install rebar (${REBAR})" ;\ 235 | curl -L -O -s https://github.com/erlang/rebar3/releases/download/${REBAR}/rebar3 ;\ 236 | chmod +x $@ 237 | 238 | -------------------------------------------------------------------------------- /priv/cache.benchmark: -------------------------------------------------------------------------------- 1 | {code_paths, ["./ebin"]}. 2 | {log_level, info}. 3 | {report_interval, 1}. 4 | {driver, cache_benchmark}. 5 | 6 | %% 7 | %% workload 8 | {mode, max}. 9 | {duration, 1}. 10 | {concurrent, 10}. 11 | {key_generator, {uniform_int, 1000000}}. 12 | {value_generator, {fixed_bin, 1000}}. 13 | 14 | {operations, [ 15 | {put, 5} 16 | ,{get, 5} 17 | ]}. 18 | 19 | {local, [ 20 | {ttl, 20} 21 | ,{n, 10} 22 | ,{policy, lru} 23 | ]}. 24 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | warnings_as_errors 3 | ]}. 4 | 5 | {deps, []}. 6 | 7 | {profiles, [ 8 | {test, [ 9 | {deps, [ 10 | meck 11 | ]} 12 | ]} 13 | ]}. 14 | 15 | %% 16 | %% 17 | {plugins , [coveralls]}. 18 | {cover_enabled , true}. 19 | {cover_export_enabled , true}. 20 | {coveralls_coverdata , "_build/test/cover/ct.coverdata"}. 21 | {coveralls_service_name , "github"}. 22 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of 2 | {"true", Token} when is_list(Token) -> 3 | CONFIG1 = [{coveralls_repo_token, Token}, 4 | {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")}, 5 | {coveralls_commit_sha, os:getenv("GITHUB_SHA")}, 6 | {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")} | CONFIG], 7 | case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request" 8 | andalso string:tokens(os:getenv("GITHUB_REF"), "/") of 9 | [_, "pull", PRNO, _] -> 10 | [{coveralls_service_pull_request, PRNO} | CONFIG1]; 11 | _ -> 12 | CONFIG1 13 | end; 14 | _ -> 15 | CONFIG 16 | end. 17 | -------------------------------------------------------------------------------- /src/cache.app.src: -------------------------------------------------------------------------------- 1 | {application, cache, 2 | [ 3 | {description, "in-memory cache"}, 4 | {vsn, "git"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel, 9 | stdlib 10 | ]}, 11 | {mod, {cache_app, []}}, 12 | {env, []}, 13 | 14 | {licenses, ["Apache"]}, 15 | {links, [{"GitHub", "https://github.com/fogfish/cache"}]} 16 | ] 17 | }. 18 | -------------------------------------------------------------------------------- /src/cache.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% segmented in-memory cache 18 | %% * cache memory is split into N segments 19 | %% * cache applies eviction and quota policies at segment level 20 | %% (e.g. whole segments is destroyed at time) 21 | %% * cache add new items to youngest segment 22 | %% * cache lookup items from youngest to oldest segment 23 | %% 24 | %% @todo 25 | %% * procedure to get / lookup multiple keys (e.g. get_, getm, ...) 26 | %% * unit tests (improve coverage) 27 | %% * cache read/write through handler 28 | %% * memcached protocol 29 | -module(cache). 30 | -author('Dmitry Kolesnikov '). 31 | 32 | -include("cache.hrl"). 33 | 34 | %% cache management interface 35 | -export([ 36 | start_link/1, 37 | start_link/2, 38 | drop/1, 39 | purge/1, 40 | i/1, 41 | i/2, 42 | heap/2 43 | ]). 44 | %% basic cache i/o interface 45 | -export([ 46 | put/3, 47 | put/4, 48 | put/5, 49 | put_/3, 50 | put_/4, 51 | put_/5, 52 | get/2, 53 | get/3, 54 | get_/2, 55 | lookup/2, 56 | lookup/3, 57 | lookup_/2, 58 | has/2, 59 | has/3, 60 | ttl/2, 61 | ttl/3, 62 | remove/2, 63 | remove/3, 64 | remove_/2, 65 | remove_/3, 66 | apply/3, 67 | apply/4, 68 | apply_/4, 69 | apply_/3 70 | ]). 71 | %% extended cache i/o interface 72 | -export([ 73 | acc/3, 74 | acc/4, 75 | acc_/3, 76 | acc_/4, 77 | set/3, 78 | set/4, 79 | set/5, 80 | set_/3, 81 | set_/4, 82 | set_/5, 83 | add/3, 84 | add/4, 85 | add/5, 86 | add_/3, 87 | add_/4, 88 | add_/5, 89 | replace/3, 90 | replace/4, 91 | replace/5, 92 | replace_/3, 93 | replace_/4, 94 | replace_/5, 95 | append/3, 96 | append/4, 97 | append_/3, 98 | append_/4, 99 | prepend/3, 100 | prepend/4, 101 | prepend_/3, 102 | prepend_/4, 103 | delete/2, 104 | delete/3, 105 | delete_/2, 106 | delete_/3 107 | ]). 108 | -export([start/0]). 109 | 110 | -export_type([cache/0]). 111 | 112 | -type(cache() :: atom() | pid()). 113 | -type(key() :: any()). 114 | -type(val() :: any()). 115 | -type(ttl() :: integer() | undefined). 116 | -type(acc() :: integer() | [{integer(), integer()}]). 117 | 118 | %% 119 | %% RnD start application 120 | start() -> 121 | application:start(cache). 122 | 123 | %%%---------------------------------------------------------------------------- 124 | %%% 125 | %%% cache management interface 126 | %%% 127 | %%%---------------------------------------------------------------------------- 128 | 129 | %% 130 | %% start new cache bucket, accepted options: 131 | %% {type, set | ordered_set} - cache segment type (default set) 132 | %% {policy, lru | mru} - cache eviction policy 133 | %% {memory, integer()} - cache memory quota in bytes 134 | %% {size, integer()} - cache cardinality quota 135 | %% {n, integer()} - number of cache segments 136 | %% {ttl, integer()} - default time-to-live in seconds 137 | %% {check, integer()} - frequency of quota check in seconds 138 | %% {stats, function() | {Mod, Fun}} - cache statistic aggregate functor 139 | %% {heir, atom() | pid()} - heir of evicted cache segments 140 | -spec(start_link(list()) -> {ok, pid()} | {error, any()}). 141 | -spec(start_link(atom(), list()) -> {ok, pid()} | {error, any()}). 142 | 143 | start_link(Opts) -> 144 | cache_bucket:start_link(Opts). 145 | 146 | start_link(Cache, Opts) -> 147 | cache_bucket:start_link(Cache, Opts). 148 | 149 | %% 150 | %% drop cache 151 | -spec(drop(cache()) -> ok). 152 | 153 | drop(Cache) -> 154 | gen_server:call(Cache, drop). 155 | 156 | %% 157 | %% purge cache 158 | -spec(purge(cache()) -> ok). 159 | 160 | purge(Cache) -> 161 | gen_server:call(Cache, purge). 162 | 163 | 164 | %% 165 | %% return cache meta data 166 | %% {heap, [integer()]} - references to cache segments 167 | %% {expire, [integer()]} - cache segments expiration times 168 | %% {size, [integer()]} - cardinality of cache segments 169 | %% {memory, [integer()]} - memory occupied by each cache segment 170 | -spec(i(cache()) -> list()). 171 | -spec(i(cache(), atom()) -> list()). 172 | 173 | i(Cache) -> 174 | gen_server:call(Cache, i). 175 | 176 | i(Cache, Name) -> 177 | proplists:get_value(Name, i(Cache)). 178 | 179 | %% 180 | %% return nth cache segment (e.g. heap(..., 1) returns youngest segment) 181 | -spec(heap(cache(), integer()) -> integer() | badarg). 182 | 183 | heap(Cache, N) -> 184 | gen_server:call(Cache, {heap, N}). 185 | 186 | 187 | %%%---------------------------------------------------------------------------- 188 | %%% 189 | %%% basic cache i/o interface 190 | %%% 191 | %%%---------------------------------------------------------------------------- 192 | 193 | %% 194 | %% synchronous cache put 195 | -spec(put(cache(), key(), val()) -> ok). 196 | -spec(put(cache(), key(), val(), ttl()) -> ok). 197 | -spec(put(cache(), key(), val(), ttl(), timeout()) -> ok). 198 | 199 | put(Cache, Key, Val) -> 200 | cache:put(Cache, Key, Val, undefined, ?CONFIG_TIMEOUT). 201 | 202 | put(Cache, Key, Val, TTL) -> 203 | cache:put(Cache, Key, Val, TTL, ?CONFIG_TIMEOUT). 204 | 205 | put(Cache, Key, Val, TTL, Timeout) -> 206 | call(Cache, {put, Key, Val, TTL}, Timeout). 207 | 208 | 209 | %% 210 | %% asynchronous cache put 211 | -spec(put_(cache(), key(), val()) -> ok | reference()). 212 | -spec(put_(cache(), key(), val(), ttl()) -> ok | reference()). 213 | -spec(put_(cache(), key(), val(), ttl(), true | false) -> ok | reference()). 214 | 215 | put_(Cache, Key, Val) -> 216 | cache:put_(Cache, Key, Val, undefined, false). 217 | 218 | put_(Cache, Key, Val, TTL) -> 219 | cache:put_(Cache, Key, Val, TTL, false). 220 | 221 | put_(Cache, Key, Val, TTL, true) -> 222 | cast(Cache, {put, Key, Val, TTL}); 223 | 224 | put_(Cache, Key, Val, TTL, false) -> 225 | send(Cache, {put, Key, Val, TTL}). 226 | 227 | %% 228 | %% synchronous cache get, the operation prolongs value ttl 229 | -spec(get(cache(), key()) -> val() | undefined). 230 | -spec(get(cache(), key(), timeout()) -> val() | undefined). 231 | 232 | get(Cache, Key) -> 233 | cache:get(Cache, Key, ?CONFIG_TIMEOUT). 234 | 235 | get(Cache, Key, Timeout) -> 236 | call(Cache, {get, Key}, Timeout). 237 | 238 | %% 239 | %% asynchronous cache get, the operation prolongs value ttl 240 | -spec(get_(cache(), key()) -> reference()). 241 | 242 | get_(Cache, Key) -> 243 | cast(Cache, {get, Key}). 244 | 245 | %% 246 | %% synchronous cache lookup, the operation do not prolong entry ttl 247 | -spec(lookup(cache(), key()) -> val() | undefined). 248 | -spec(lookup(cache(), key(), timeout()) -> val() | undefined). 249 | 250 | lookup(Cache, Key) -> 251 | cache:lookup(Cache, Key, ?CONFIG_TIMEOUT). 252 | 253 | lookup(Cache, Key, Timeout) -> 254 | call(Cache, {lookup, Key}, Timeout). 255 | 256 | %% 257 | %% asynchronous cache lookup, the operation do not prolong entry ttl 258 | -spec(lookup_(cache(), key()) -> reference()). 259 | 260 | lookup_(Cache, Key) -> 261 | cast(Cache, {lookup, Key}). 262 | 263 | %% 264 | %% check if cache key exists, 265 | -spec(has(cache(), key()) -> true | false). 266 | -spec(has(cache(), key(), timeout()) -> true | false). 267 | 268 | has(Cache, Key) -> 269 | cache:has(Cache, Key, ?CONFIG_TIMEOUT). 270 | 271 | has(Cache, Key, Timeout) -> 272 | call(Cache, {has, Key}, Timeout). 273 | 274 | %% 275 | %% check entity at cache and return estimated ttl 276 | -spec(ttl(cache(), key()) -> ttl() | false). 277 | -spec(ttl(cache(), key(), timeout()) -> ttl() | false). 278 | 279 | ttl(Cache, Key) -> 280 | cache:ttl(Cache, Key, ?CONFIG_TIMEOUT). 281 | 282 | ttl(Cache, Key, Timeout) -> 283 | call(Cache, {ttl, Key}, Timeout). 284 | 285 | %% 286 | %% synchronous remove entry from cache 287 | -spec(remove(cache(), key()) -> ok). 288 | -spec(remove(cache(), key(), timeout()) -> ok). 289 | 290 | remove(Cache, Key) -> 291 | cache:remove(Cache, Key, ?CONFIG_TIMEOUT). 292 | 293 | remove(Cache, Key, Timeout) -> 294 | call(Cache, {remove, Key}, Timeout). 295 | 296 | %% 297 | %% asynchronous remove entry from cache 298 | -spec(remove_(cache(), key()) -> ok | reference()). 299 | -spec(remove_(cache(), key(), true | false) -> ok | reference()). 300 | 301 | remove_(Cache, Key) -> 302 | cache:remove_(Cache, Key, false). 303 | 304 | remove_(Cache, Key, true) -> 305 | cast(Cache, {remove, Key}); 306 | 307 | remove_(Cache, Key, false) -> 308 | send(Cache, {remove, Key}). 309 | 310 | %% 311 | %% synchronous apply function to entity on cache 312 | %% the function maps element, the new value is returned 313 | %% the operation prolongs value ttl 314 | -spec apply(cache(), key(), fun((_) -> _)) -> val() | undefined. 315 | -spec apply(cache(), key(), fun((_) -> _), timeout()) -> val() | undefined. 316 | 317 | apply(Cache, Key, Fun) -> 318 | cache:apply(Cache, Key, Fun, ?CONFIG_TIMEOUT). 319 | 320 | apply(Cache, Key, Fun, Timeout) -> 321 | call(Cache, {apply, Key, Fun}, Timeout). 322 | 323 | 324 | %% 325 | %% asynchronous apply function to entity on cache 326 | %% the function maps element, the new value is returned 327 | %% the operation prolongs value ttl 328 | -spec apply_(cache(), key(), fun((_) -> _)) -> ok | reference(). 329 | -spec apply_(cache(), key(), fun((_) -> _), true | false) -> ok | reference(). 330 | 331 | apply_(Cache, Key, Fun) -> 332 | cache:apply_(Cache, Key, Fun, false). 333 | 334 | apply_(Cache, Key, Fun, true) -> 335 | cast(Cache, {apply, Key, Fun}); 336 | 337 | apply_(Cache, Key, Fun, false) -> 338 | cast(Cache, {apply, Key, Fun}). 339 | 340 | %%%---------------------------------------------------------------------------- 341 | %%% 342 | %%% extended cache i/o interface 343 | %%% 344 | %%%---------------------------------------------------------------------------- 345 | 346 | %% 347 | %% synchronous in-cache accumulator 348 | -spec(acc(cache(), key(), acc()) -> integer() | undefined). 349 | -spec(acc(cache(), key(), acc(), timeout()) -> integer() | undefined). 350 | 351 | acc(Cache, Key, Val) -> 352 | cache:acc(Cache, Key, Val, ?CONFIG_TIMEOUT). 353 | 354 | acc(Cache, Key, Val, Timeout) -> 355 | call(Cache, {acc, Key, Val}, Timeout). 356 | 357 | %% 358 | %% asynchronous in-cache accumulator 359 | -spec(acc_(cache(), key(), acc()) -> ok | reference()). 360 | -spec(acc_(cache(), key(), acc(), true | false) -> ok). 361 | 362 | acc_(Cache, Key, Val) -> 363 | cache:acc_(Cache, Key, Val, false). 364 | 365 | acc_(Cache, Key, Val, true) -> 366 | cast(Cache, {acc, Key, Val}); 367 | 368 | acc_(Cache, Key, Val, false) -> 369 | send(Cache, {acc, Key, Val}). 370 | 371 | %% 372 | %% synchronous store key/val 373 | -spec(set(cache(), key(), val()) -> ok). 374 | -spec(set(cache(), key(), val(), ttl()) -> ok). 375 | -spec(set(cache(), key(), val(), ttl(), timeout()) -> ok). 376 | 377 | set(Cache, Key, Val) -> 378 | cache:put(Cache, Key, Val). 379 | 380 | set(Cache, Key, Val, TTL) -> 381 | cache:put(Cache, Key, Val, TTL). 382 | 383 | set(Cache, Key, Val, TTL, Timeout) -> 384 | cache:put(Cache, Key, Val, TTL, Timeout). 385 | 386 | %% 387 | %% asynchronous store key/val 388 | -spec(set_(cache(), key(), val()) -> ok | reference()). 389 | -spec(set_(cache(), key(), val(), ttl()) -> ok | reference()). 390 | -spec(set_(cache(), key(), val(), ttl(), true | false) -> ok | reference()). 391 | 392 | set_(Cache, Key, Val) -> 393 | cache:put_(Cache, Key, Val). 394 | 395 | set_(Cache, Key, Val, TTL) -> 396 | cache:put_(Cache, Key, Val, TTL). 397 | 398 | set_(Cache, Key, Val, TTL, Flag) -> 399 | cache:put_(Cache, Key, Val, TTL, Flag). 400 | 401 | %% 402 | %% synchronous store key/val only if cache does not already hold data for this key 403 | -spec(add(cache(), key(), val()) -> ok | {error, conflict}). 404 | -spec(add(cache(), key(), val(), ttl()) -> ok | {error, conflict}). 405 | -spec(add(cache(), key(), val(), ttl(), timeout()) -> ok | {error, conflict}). 406 | 407 | add(Cache, Key, Val) -> 408 | cache:add(Cache, Key, Val, undefined, ?CONFIG_TIMEOUT). 409 | 410 | add(Cache, Key, Val, TTL) -> 411 | cache:add(Cache, Key, Val, TTL, ?CONFIG_TIMEOUT). 412 | 413 | add(Cache, Key, Val, TTL, Timeout) -> 414 | call(Cache, {add, Key, Val, TTL}, Timeout). 415 | 416 | %% 417 | %% asynchronous store key/val only if cache does not already hold data for this key 418 | -spec(add_(cache(), key(), val()) -> ok | reference()). 419 | -spec(add_(cache(), key(), val(), ttl()) -> ok | reference()). 420 | -spec(add_(cache(), key(), val(), ttl(), true | false) -> ok | reference()). 421 | 422 | add_(Cache, Key, Val) -> 423 | cache:add_(Cache, Key, Val, undefined, false). 424 | 425 | add_(Cache, Key, Val, TTL) -> 426 | cache:add_(Cache, Key, Val, TTL, false). 427 | 428 | add_(Cache, Key, Val, TTL, true) -> 429 | cast(Cache, {add, Key, Val, TTL}); 430 | 431 | add_(Cache, Key, Val, TTL, false) -> 432 | send(Cache, {add, Key, Val, TTL}). 433 | 434 | %% 435 | %% synchronous store key/val only if cache does hold data for this key 436 | -spec(replace(cache(), key(), val()) -> ok | {error, not_found}). 437 | -spec(replace(cache(), key(), val(), ttl()) -> ok | {error, not_found}). 438 | -spec(replace(cache(), key(), val(), ttl(), timeout()) -> ok | {error, not_found}). 439 | 440 | replace(Cache, Key, Val) -> 441 | cache:replace(Cache, Key, Val, undefined, ?CONFIG_TIMEOUT). 442 | 443 | replace(Cache, Key, Val, TTL) -> 444 | cache:replace(Cache, Key, Val, TTL, ?CONFIG_TIMEOUT). 445 | 446 | replace(Cache, Key, Val, TTL, Timeout) -> 447 | call(Cache, {replace, Key, Val, TTL}, Timeout). 448 | 449 | %% 450 | %% asynchronous store key/val only if cache does hold data for this key 451 | -spec(replace_(cache(), key(), val()) -> ok | reference()). 452 | -spec(replace_(cache(), key(), val(), ttl()) -> ok | reference()). 453 | -spec(replace_(cache(), key(), val(), ttl(), true | false) -> ok | reference()). 454 | 455 | replace_(Cache, Key, Val) -> 456 | cache:replace_(Cache, Key, Val, undefined, false). 457 | 458 | replace_(Cache, Key, Val, TTL) -> 459 | cache:replace_(Cache, Key, Val, TTL, false). 460 | 461 | replace_(Cache, Key, Val, TTL, true) -> 462 | cast(Cache, {replace, Key, Val, TTL}); 463 | 464 | replace_(Cache, Key, Val, TTL, false) -> 465 | send(Cache, {replace, Key, Val, TTL}). 466 | 467 | 468 | %% 469 | %% synchronously add data to existing key after existing data, 470 | %% the operation do not prolong entry ttl 471 | -spec(append(cache(), key(), val()) -> ok | {error, not_found}). 472 | -spec(append(cache(), key(), val(), timeout()) -> ok | {error, not_found}). 473 | 474 | append(Cache, Key, Val) -> 475 | cache:append(Cache, Key, Val, ?CONFIG_TIMEOUT). 476 | 477 | append(Cache, Key, Val, Timeout) -> 478 | call(Cache, {append, Key, Val}, Timeout). 479 | 480 | %% 481 | %% asynchronously add data to existing key after existing data, 482 | %% the operation do not prolong entry ttl 483 | -spec(append_(cache(), key(), val()) -> ok | reference()). 484 | -spec(append_(cache(), key(), val(), true | false) -> ok | reference()). 485 | 486 | append_(Cache, Key, Val) -> 487 | cache:append_(Cache, Key, Val, false). 488 | 489 | append_(Cache, Key, Val, true) -> 490 | cast(Cache, {append, Key, Val}); 491 | append_(Cache, Key, Val, false) -> 492 | send(Cache, {append, Key, Val}). 493 | 494 | 495 | %% 496 | %% synchronously add data to existing key before existing data 497 | %% the operation do not prolong entry ttl 498 | -spec(prepend(cache(), key(), val()) -> ok | {error, not_found}). 499 | -spec(prepend(cache(), key(), val(), timeout()) -> ok | {error, not_found}). 500 | 501 | prepend(Cache, Key, Val) -> 502 | cache:prepend(Cache, Key, Val, ?CONFIG_TIMEOUT). 503 | 504 | prepend(Cache, Key, Val, Timeout) -> 505 | call(Cache, {prepend, Key, Val}, Timeout). 506 | 507 | %% 508 | %% asynchronously add data to existing key before existing data 509 | %% the operation do not prolong entry ttl 510 | -spec(prepend_(cache(), key(), val()) -> reference()). 511 | -spec(prepend_(cache(), key(), val(), true | false) -> reference()). 512 | 513 | prepend_(Cache, Key, Val) -> 514 | cache:prepend_(Cache, Key, Val, false). 515 | 516 | prepend_(Cache, Key, Val, true) -> 517 | cast(Cache, {prepend, Key, Val}); 518 | 519 | prepend_(Cache, Key, Val, false) -> 520 | send(Cache, {prepend, Key, Val}). 521 | 522 | %% 523 | %% synchronous remove entry from cache 524 | -spec(delete(cache(), key()) -> ok). 525 | -spec(delete(cache(), key(), timeout()) -> ok). 526 | 527 | delete(Cache, Key) -> 528 | cache:remove(Cache, Key). 529 | 530 | delete(Cache, Key, Timeout) -> 531 | cache:remove(Cache, Key, Timeout). 532 | 533 | %% 534 | %% asynchronous remove entry from cache 535 | -spec(delete_(cache(), key()) -> ok | reference()). 536 | -spec(delete_(cache(), key(), true | false) -> ok | reference()). 537 | 538 | delete_(Cache, Key) -> 539 | cache:remove_(Cache, Key). 540 | 541 | delete_(Cache, Key, Flag) -> 542 | cache:remove_(Cache, Key, Flag). 543 | 544 | 545 | %%%---------------------------------------------------------------------------- 546 | %%% 547 | %%% private 548 | %%% 549 | %%%---------------------------------------------------------------------------- 550 | 551 | %% 552 | %% synchronous call to server, client is blocks 553 | call(Pid, Req, Timeout) -> 554 | gen_server:call(Pid, Req, Timeout). 555 | 556 | %% 557 | %% asynchronous call to server, 558 | %% the request is acknowledged using reference 559 | cast(Pid, Req) -> 560 | Ref = erlang:make_ref(), 561 | erlang:send(Pid, {'$gen_call', {self(), Ref}, Req}, [noconnect]), 562 | Ref. 563 | 564 | %% 565 | %% fire-and-forget 566 | send(Pid, Req) -> 567 | gen_server:cast(Pid, Req). 568 | 569 | -------------------------------------------------------------------------------- /src/cache.hrl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | 17 | % -define(VERBOSE, true). 18 | -ifdef(VERBOSE). 19 | -define(DEBUG(Str, Args), error_logger:error_msg(Str, Args)). 20 | -else. 21 | -define(DEBUG(Str, Args), ok). 22 | -endif. 23 | 24 | %% default cache eviction policy 25 | -define(DEF_CACHE_POLICY, lru). 26 | 27 | %% default cache ttl and number of generations 28 | -define(DEF_CACHE_TYPE, set). 29 | -define(DEF_CACHE_TTL, 600). 30 | -define(DEF_CACHE_N, 10). 31 | 32 | %% default cache house keeping frequency 33 | -define(DEF_CACHE_CHECK, 20000). 34 | 35 | %% default cache i/o timeout 36 | -define(CONFIG_TIMEOUT, 30000). 37 | -------------------------------------------------------------------------------- /src/cache_app.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% cache application 18 | -module(cache_app). 19 | -behaviour(application). 20 | -author('Dmitry Kolesnikov '). 21 | 22 | -export([start/2, stop/1]). 23 | 24 | start(_Type, _Args) -> 25 | cache_sup:start_link(). 26 | 27 | stop(_State) -> 28 | ok. -------------------------------------------------------------------------------- /src/cache_benchmark.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% cache basho_bench driver 18 | -module(cache_benchmark). 19 | -author('Dmitry Kolesnikov '). 20 | 21 | -include_lib("stdlib/include/ms_transform.hrl"). 22 | 23 | -export([new/1, run/4]). 24 | 25 | %% 26 | %% 27 | new(_Id) -> 28 | try 29 | lager:set_loglevel(lager_console_backend, basho_bench_config:get(log_level, info)), 30 | case basho_bench_config:get(cache, undefined) of 31 | undefined -> {ok, local_init()}; 32 | Cache -> {ok, Cache} 33 | end 34 | catch _:Err -> 35 | error_logger:error_msg("cache failed: ~p", [Err]), 36 | halt(1) 37 | end. 38 | 39 | %% 40 | %% 41 | run(put, KeyGen, ValGen, Cache) -> 42 | Key = KeyGen(), 43 | case (catch cache:put(Cache, Key, ValGen())) of 44 | ok -> {ok, Cache}; 45 | E -> {error, failure(p, Key, E), Cache} 46 | end; 47 | 48 | run(get, KeyGen, _ValGen, Cache) -> 49 | Key = KeyGen(), 50 | case (catch cache:get(Cache, Key)) of 51 | Val when is_binary(Val) -> {ok, Cache}; 52 | undefined -> {ok, Cache}; 53 | E -> {error, failure(g, Key, E), Cache} 54 | end; 55 | 56 | run(remove, KeyGen, _ValGen, Cache) -> 57 | Key = KeyGen(), 58 | case (catch cache:remove(cache, Key)) of 59 | ok -> {ok, Cache}; 60 | E -> {error, failure(r, Key, E), Cache} 61 | end. 62 | 63 | %% 64 | local_init() -> 65 | case cache:start() of 66 | {error, {already_started, _}} -> 67 | cache; 68 | ok -> 69 | Cache = basho_bench_config:get(local, undefined), 70 | {ok, _} = cache:start_link(cache, Cache), 71 | cache 72 | end. 73 | 74 | %% 75 | %% 76 | failure(_Tag, _Key, _E) -> 77 | %io:format("~s -> ~p~n", [Tag, E]), 78 | failed. 79 | -------------------------------------------------------------------------------- /src/cache_bucket.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% cache bucket process 18 | -module(cache_bucket). 19 | -behaviour(gen_server). 20 | -author('Dmitry Kolesnikov '). 21 | 22 | -include("cache.hrl"). 23 | 24 | -export([ 25 | start_link/1, 26 | start_link/2, 27 | init/1, 28 | terminate/2, 29 | handle_call/3, 30 | handle_cast/2, 31 | handle_info/2, 32 | code_change/3 33 | ]). 34 | 35 | %% internal bucket state 36 | -record(cache, { 37 | name = undefined :: atom() %% name of cache bucket 38 | ,heap = undefined :: list() %% cache heap segments 39 | ,policy = ?DEF_CACHE_POLICY :: atom() %% eviction policy 40 | ,check = ?DEF_CACHE_CHECK :: integer() %% status check timeout 41 | ,evict = undefined :: integer() %% evict timeout 42 | ,stats = undefined :: any() %% stats aggregation functor 43 | ,heir = undefined :: pid() %% the heir of evicted cache segment 44 | }). 45 | 46 | %%%---------------------------------------------------------------------------- 47 | %%% 48 | %%% Factory 49 | %%% 50 | %%%---------------------------------------------------------------------------- 51 | 52 | start_link(Opts) -> 53 | gen_server:start_link(?MODULE, [undefined, Opts], []). 54 | 55 | start_link({global, Name}, Opts) -> 56 | gen_server:start_link({global, Name}, ?MODULE, [Name, Opts], []); 57 | 58 | start_link(Name, Opts) -> 59 | gen_server:start_link({local, Name}, ?MODULE, [Name, Opts], []). 60 | 61 | init([Name, Opts]) -> 62 | {ok, init(Opts, Opts, #cache{name=Name})}. 63 | 64 | init([{policy, X} | Tail], Opts, State) -> 65 | init(Tail, Opts, State#cache{policy=X}); 66 | init([{check, X} | Tail], Opts, State) -> 67 | init(Tail, Opts, State#cache{check=X * 1000}); 68 | init([{stats, X} | Tail], Opts, State) -> 69 | init(Tail, Opts, State#cache{stats=X}); 70 | init([{heir, X} | Tail], Opts, State) -> 71 | init(Tail, Opts, State#cache{heir=X}); 72 | init([_ | Tail], Opts, State) -> 73 | init(Tail, Opts, State); 74 | init([], Opts, #cache{check = Check} = State) -> 75 | N = proplists:get_value(n, Opts, ?DEF_CACHE_N), 76 | Type = proplists:get_value(type, Opts, ?DEF_CACHE_TYPE), 77 | TTL = proplists:get_value(ttl, Opts, ?DEF_CACHE_TTL), 78 | Size = proplists:get_value(size, Opts), 79 | Mem = proplists:get_value(memory, Opts), 80 | Evict= cache_util:mmul(cache_util:mdiv(TTL, N), 1000), 81 | Heap = cache_heap:new(Type, N, TTL, Size, Mem), 82 | (catch erlang:send_after(Check, self(), check_heap)), 83 | (catch erlang:send_after(Evict, self(), evict_heap)), 84 | State#cache{heap=Heap, evict=Evict}. 85 | 86 | %% 87 | %% 88 | terminate(_Reason, #cache{heir = Heir, heap = Heap}) -> 89 | cache_heap:purge(Heir, Heap), 90 | ok. 91 | 92 | 93 | %%%---------------------------------------------------------------------------- 94 | %%% 95 | %%% gen_server 96 | %%% 97 | %%%---------------------------------------------------------------------------- 98 | 99 | handle_call({put, Key, Val, TTL}, _, State) -> 100 | {reply, ok, cache_put(Key, Val, TTL, State)}; 101 | 102 | handle_call({get, Key}, _, State) -> 103 | {reply, cache_get(Key, State), State}; 104 | 105 | handle_call({lookup, Key}, _, State) -> 106 | {reply, cache_lookup(Key, State), State}; 107 | 108 | handle_call({has, Key}, _, State) -> 109 | {reply, cache_has(Key, State), State}; 110 | 111 | handle_call({ttl, Key}, _, State) -> 112 | {reply, cache_ttl(Key, State), State}; 113 | 114 | handle_call({remove, Key}, _, State) -> 115 | {reply, ok, cache_remove(Key, State)}; 116 | 117 | handle_call({apply, Key, Fun}, _, State0) -> 118 | {Result, State1} = cache_apply(Key, Fun, State0), 119 | {reply, Result, State1}; 120 | 121 | handle_call({acc, Key, Val}, _, State0) -> 122 | {Result, State1} = cache_acc(Key, Val, State0), 123 | {reply, Result, State1}; 124 | 125 | handle_call({add, Key, Val, TTL}, _, State0) -> 126 | {Result, State1} = cache_add(Key, Val, TTL, State0), 127 | {reply, Result, State1}; 128 | 129 | handle_call({replace, Key, Val, TTL}, _, State0) -> 130 | {Result, State1} = cache_replace(Key, Val, TTL, State0), 131 | {reply, Result, State1}; 132 | 133 | handle_call({prepend, Key, Val}, _, State0) -> 134 | {Result, State1} = cache_prepend(Key, Val, State0), 135 | {reply, Result, State1}; 136 | 137 | handle_call({append, Key, Val}, _, State0) -> 138 | {Result, State1} = cache_append(Key, Val, State0), 139 | {reply, Result, State1}; 140 | 141 | handle_call(i, _, State) -> 142 | Heap = cache_heap:refs(State#cache.heap), 143 | Refs = [X || {_, X} <- Heap], 144 | Expire = [X || {X, _} <- Heap], 145 | Size = [ets:info(X, size) || {_, X} <- Heap], 146 | Memory = [ets:info(X, memory) || {_, X} <- Heap], 147 | {reply, [{heap, Refs}, {expire, Expire}, {size, Size}, {memory, Memory}], State}; 148 | 149 | handle_call({heap, N}, _, State) -> 150 | try 151 | {_, Ref} = lists:nth(N, cache_heap:refs(State#cache.heap)), 152 | {reply, Ref, State} 153 | catch _:_ -> 154 | {reply, badarg, State} 155 | end; 156 | 157 | handle_call(drop, _, State) -> 158 | {stop, normal, ok, State}; 159 | 160 | handle_call(purge, _, #cache{heir = Heir, heap = Heap} = State) -> 161 | {reply, ok, 162 | State#cache{ 163 | heap = cache_heap:purge(Heir, Heap) 164 | } 165 | }; 166 | 167 | handle_call(_, _, State) -> 168 | {noreply, State}. 169 | 170 | %% 171 | %% 172 | handle_cast({put, Key, Val, TTL}, State) -> 173 | {noreply, cache_put(Key, Val, TTL, State)}; 174 | 175 | handle_cast({remove, Key}, State) -> 176 | {noreply, cache_remove(Key, State)}; 177 | 178 | handle_cast({acc, Key, Val}, State0) -> 179 | {_, State1} = cache_acc(Key, Val, State0), 180 | {noreply, State1}; 181 | 182 | handle_cast({add, Key, Val, TTL}, State0) -> 183 | {_, State1} = cache_add(Key, Val, TTL, State0), 184 | {noreply, State1}; 185 | 186 | handle_cast({replace, Key, Val, TTL}, State0) -> 187 | {_, State1} = cache_replace(Key, Val, TTL, State0), 188 | {noreply, State1}; 189 | 190 | handle_cast({prepend, Key, Val}, State0) -> 191 | {_, State1} = cache_prepend(Key, Val, State0), 192 | {noreply, State1}; 193 | 194 | handle_cast({append, Key, Val}, State0) -> 195 | {_, State1} = cache_append(Key, Val, State0), 196 | {noreply, State1}; 197 | 198 | handle_cast(_, State) -> 199 | {noreply, State}. 200 | 201 | %% 202 | %% 203 | handle_info(check_heap, #cache{check = Check, heir = Heir, heap = Heap0} = State) -> 204 | erlang:send_after(Check, self(), check_heap), 205 | case cache_heap:slip(Heir, Heap0) of 206 | {ok, Heap1} -> 207 | {noreply, State#cache{heap = Heap1}}; 208 | {Reason, Heap1} -> 209 | cache_util:stats(State#cache.stats, {cache, State#cache.name, Reason}), 210 | {noreply, State#cache{heap = Heap1}} 211 | end; 212 | 213 | 214 | handle_info(evict_heap, #cache{evict = Evict, heir = Heir, heap = Heap0} = State) -> 215 | erlang:send_after(Evict, self(), evict_heap), 216 | case cache_heap:slip(Heir, Heap0) of 217 | {ok, Heap1} -> 218 | {noreply, State#cache{heap = Heap1}}; 219 | {Reason, Heap1} -> 220 | cache_util:stats(State#cache.stats, {cache, State#cache.name, Reason}), 221 | {noreply, State#cache{heap = Heap1}} 222 | end; 223 | 224 | handle_info(_, S) -> 225 | {noreply, S}. 226 | 227 | %% 228 | %% 229 | code_change(_Vsn, S, _Extra) -> 230 | {ok, S}. 231 | 232 | %%%---------------------------------------------------------------------------- 233 | %%% 234 | %%% private 235 | %%% 236 | %%%---------------------------------------------------------------------------- 237 | 238 | %% 239 | %% insert value to cache 240 | cache_put(Key, Val, undefined, #cache{name = _Name, heap = Heap} = State) -> 241 | {{_, Head}, Tail} = cache_heap:split(Heap), 242 | true = ets:insert(Head, {Key, Val}), 243 | ok = heap_remove(Key, Tail), 244 | _ = stats(put, State), 245 | ?DEBUG("cache ~p: put ~p to heap ~p~n", [_Name, Key, Head]), 246 | State; 247 | 248 | cache_put(Key, Val, TTL, #cache{name = _Name, heap = Heap} = State) -> 249 | {{_, Head}, Tail} = cache_heap:split(cache_util:now() + TTL, Heap), 250 | true = ets:insert(Head, {Key, Val}), 251 | ok = heap_remove(Key, Tail), 252 | _ = stats(put, State), 253 | ?DEBUG("cache ~p: put ~p to heap ~p~n", [_Name, Key, Head]), 254 | State. 255 | 256 | %% 257 | %% get cache value 258 | cache_get(Key, #cache{policy = mru} = State) -> 259 | % cache MRU should not move key anywhere because 260 | % cache always evicts last generation 261 | % fall-back to cache lookup 262 | cache_lookup(Key, State); 263 | 264 | cache_get(Key, #cache{name = _Name, heap = Heap} = State) -> 265 | {{_, Head}, Tail} = cache_heap:split(Heap), 266 | case heap_lookup(Key, Head, Tail) of 267 | undefined -> 268 | stats(miss, State), 269 | undefined; 270 | {Head, Val} -> 271 | ?DEBUG("cache ~p: get ~p at cell ~p~n", [_Name, Key, Head]), 272 | stats(hit, State), 273 | Val; 274 | {Cell, Val} -> 275 | true = ets:insert(Head, {Key, Val}), 276 | _ = ets:delete(Cell, Key), 277 | ?DEBUG("cache ~p: get ~p at cell ~p~n", [_Name, Key, Cell]), 278 | stats(hit, State), 279 | Val 280 | end. 281 | 282 | %% 283 | %% lookup cache value 284 | cache_lookup(Key, #cache{name = _Name, heap = Heap} = State) -> 285 | {{_, Head}, Tail} = cache_heap:split(Heap), 286 | case heap_lookup(Key, Head, Tail) of 287 | undefined -> 288 | stats(miss, State), 289 | undefined; 290 | {_Cell, Val} -> 291 | ?DEBUG("cache ~p: get ~p at cell ~p~n", [_Name, Key, _Cell]), 292 | stats(hit, State), 293 | Val 294 | end. 295 | 296 | %% 297 | %% check if key exists 298 | cache_has(Key, #cache{name=_Name, heap=Heap}) -> 299 | {Head, Tail} = cache_heap:split(Heap), 300 | case heap_has(Key, Head, Tail) of 301 | false -> 302 | false; 303 | _Heap -> 304 | ?DEBUG("cache ~p: has ~p at cell ~p~n", [_Name, Key, _Heap]), 305 | true 306 | end. 307 | 308 | %% 309 | %% check key ttl 310 | cache_ttl(Key,#cache{heap = Heap}) -> 311 | {Head, Tail} = cache_heap:split(Heap), 312 | case heap_has(Key, Head, Tail) of 313 | false -> 314 | undefined; 315 | {Expire, _} -> 316 | Expire - cache_util:now() 317 | end. 318 | 319 | %% 320 | %% 321 | cache_remove(Key, #cache{name = _Name, heap = Heap} = State) -> 322 | {{_, Head}, Tail} = cache_heap:split(Heap), 323 | ok = heap_remove(Key, Head, Tail), 324 | _ = stats(remove, State), 325 | ?DEBUG("cache ~p: remove ~p~n", [_Name, Key]), 326 | State. 327 | 328 | %% 329 | %% 330 | cache_apply(Key, Fun, State) -> 331 | Old = cache_get(Key, State), 332 | case Fun(Old) of 333 | undefined -> 334 | {undefined, State}; 335 | Old -> 336 | {Old, State}; 337 | Val -> 338 | {Val, cache_put(Key, Val, undefined, State)} 339 | end. 340 | 341 | %% 342 | %% @todo: reduce one write 343 | cache_acc(Key, Val, State) 344 | when is_integer(Val) -> 345 | case cache_get(Key, State) of 346 | undefined -> 347 | {undefined, cache_put(Key, Val, undefined, State)}; 348 | X when is_integer(X) -> 349 | {X, cache_put(Key, X + Val, undefined, State)}; 350 | X when is_tuple(X) -> 351 | {erlang:element(1, X), cache_put(Key, tuple_acc({1, Val}, X), undefined, State)}; 352 | _ -> 353 | {badarg, State} 354 | end; 355 | cache_acc(Key, Val, State) -> 356 | case cache_get(Key, State) of 357 | X when is_tuple(X) -> 358 | {X, cache_put(Key, tuple_acc(Val, X), undefined, State)}; 359 | _ -> 360 | {badarg, State} 361 | end. 362 | 363 | tuple_acc({Pos, Val}, X) -> 364 | erlang:setelement(Pos, X, erlang:element(Pos, X) + Val); 365 | tuple_acc(List, X) -> 366 | lists:foldl( 367 | fun({Pos, Val}, Acc) -> 368 | erlang:setelement(Pos, Acc, erlang:element(Pos, Acc) + Val) 369 | end, 370 | X, 371 | List 372 | ). 373 | 374 | 375 | %% 376 | %% 377 | cache_add(Key, Val, TTL, State) -> 378 | case cache_has(Key, State) of 379 | true -> 380 | {{error, conflict}, State}; 381 | false -> 382 | {ok, cache_put(Key, Val, TTL, State)} 383 | end. 384 | 385 | %% 386 | %% 387 | cache_replace(Key, Val, TTL, State) -> 388 | case cache_has(Key, State) of 389 | true -> 390 | {ok, cache_put(Key, Val, TTL, State)}; 391 | false -> 392 | {{error, not_found}, State} 393 | end. 394 | 395 | %% 396 | %% 397 | cache_prepend(Key, Val, State) -> 398 | % @todo: reduce one write 399 | case cache_get(Key, State) of 400 | undefined -> 401 | {ok, cache_put(Key, [Val], undefined, State)}; 402 | X when is_list(X) -> 403 | {ok, cache_put(Key, [Val|X], undefined, State)}; 404 | X -> 405 | {ok, cache_put(Key, [Val,X], undefined, State)} 406 | end. 407 | 408 | %% 409 | %% 410 | cache_append(Key, Val, State) -> 411 | % @todo: reduce one write 412 | case cache_get(Key, State) of 413 | undefined -> 414 | {ok, cache_put(Key, [Val], undefined, State)}; 415 | X when is_list(X) -> 416 | {ok, cache_put(Key, X++[Val], undefined, State)}; 417 | X -> 418 | {ok, cache_put(Key, [X, Val], undefined, State)} 419 | end. 420 | 421 | 422 | %% 423 | %% remove key from heap segments 424 | heap_remove(Key, Heap, Tail) -> 425 | ets:delete(Heap, Key), 426 | heap_remove(Key, Tail). 427 | 428 | heap_remove(Key, {Tail, Head}) -> 429 | heap_remove(Key, Tail), 430 | heap_remove(Key, Head); 431 | 432 | heap_remove(Key, Heap) -> 433 | lists:foreach( 434 | fun({_, Id}) -> ets:delete(Id, Key) end, 435 | Heap 436 | ). 437 | 438 | %% 439 | %% 440 | heap_lookup(Key, Heap, Tail) -> 441 | case ets:lookup(Heap, Key) of 442 | [] -> heap_lookup(Key, Tail); 443 | [{_, Val}] -> {Heap, Val} 444 | end. 445 | 446 | heap_lookup(Key, {Tail, Head}) -> 447 | case heap_lookup(Key, Tail) of 448 | undefined -> 449 | heap_lookup(Key, Head); 450 | Hit -> 451 | Hit 452 | end; 453 | 454 | heap_lookup(Key, [{_, Heap} | Tail]) -> 455 | case ets:lookup(Heap, Key) of 456 | [] -> heap_lookup(Key, Tail); 457 | [{_, Val}] -> {Heap, Val} 458 | end; 459 | 460 | heap_lookup(_Key, []) -> 461 | undefined. 462 | 463 | %% 464 | %% 465 | heap_has(Key, {_, Heap} = X, Tail) -> 466 | case ets:lookup(Heap, Key) of 467 | [] -> heap_has(Key, Tail); 468 | [{_, _}] -> X 469 | end. 470 | 471 | heap_has(Key, {Tail, Head}) -> 472 | case heap_has(Key, Tail) of 473 | false -> 474 | heap_has(Key, Head); 475 | Hit -> 476 | Hit 477 | end; 478 | 479 | heap_has(Key, [{_, Heap} = X | Tail]) -> 480 | case ets:member(Heap, Key) of 481 | false -> heap_has(Key, Tail); 482 | true -> X 483 | end; 484 | 485 | heap_has(_Key, []) -> 486 | false. 487 | 488 | %% 489 | %% update statistic 490 | stats(_, #cache{stats = undefined}) -> 491 | ok; 492 | stats(X, #cache{stats = Stats, name = Name}) -> 493 | cache_util:stats(Stats, {cache, Name, X}). 494 | 495 | -------------------------------------------------------------------------------- /src/cache_bucket_sup.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% root cache supervisor 18 | -module(cache_bucket_sup). 19 | -behaviour(supervisor). 20 | -author('Dmitry Kolesnikov '). 21 | -author('Jose Luis Navarro '). 22 | 23 | %% API 24 | -export([start_link/0]). 25 | 26 | %% Supervisor callbacks 27 | -export([init/1]). 28 | 29 | %% 30 | -define(CHILD(Type, I), {I, {I, start_link, []}, permanent, 5000, Type, dynamic}). 31 | -define(CHILD(Type, I, Args), {I, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 32 | -define(CHILD(Type, ID, I, Args), {ID, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 33 | 34 | %% =================================================================== 35 | %% API functions 36 | %% =================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | %% =================================================================== 42 | %% Supervisor callbacks 43 | %% =================================================================== 44 | 45 | init([]) -> 46 | {ok, 47 | { 48 | {one_for_one, 4, 1800}, 49 | [?CHILD(worker, Name, cache, [Name, Opts]) || {Name, Opts} <- default_cache()] 50 | } 51 | }. 52 | 53 | %% 54 | %% list of default cache specification 55 | %% Note, following keys are reserved 56 | %% * included_applications 57 | %% * cache_shards 58 | default_cache() -> 59 | [Env || {Name, _} = Env <- application:get_all_env(), 60 | Name /= included_applications, 61 | Name /= cache_shards 62 | ]. 63 | -------------------------------------------------------------------------------- /src/cache_heap.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% cache segmented heap 18 | -module(cache_heap). 19 | 20 | -export([ 21 | new/5 22 | , refs/1 23 | , split/1 24 | , split/2 25 | , slip/2 26 | , purge/2 27 | ]). 28 | 29 | %% 30 | -type segment() :: [{integer(), reference()}]. 31 | -type heir() :: undefined | atom() | pid(). 32 | -type queue() :: {[segment()], [segment()]}. 33 | 34 | -record(heap, { 35 | type = set :: atom() %% type of segment 36 | , n = undefined :: integer() %% number of segments 37 | , ttl = undefined :: integer() %% segment expire time 38 | , cardinality = undefined :: integer() %% segment cardinality quota 39 | , memory = undefined :: integer() %% segment memory quota 40 | , segments = undefined :: queue() %% segment references 41 | }). 42 | 43 | %% 44 | %% create new empty heap 45 | -spec new(atom(), integer(), integer(), integer(), integer()) -> #heap{}. 46 | 47 | new(Type, N, TTL, Cardinality, Memory) -> 48 | init(#heap{ 49 | type = Type 50 | , n = N 51 | , ttl = TTL div N 52 | , cardinality = cache_util:mdiv(Cardinality, N) 53 | , memory = cache_util:mdiv(cache_util:mdiv(Memory, N), erlang:system_info(wordsize)) 54 | }). 55 | 56 | init(#heap{type = Type, n = N, ttl = TTL} = Heap) -> 57 | T = cache_util:now(), 58 | Segments = lists:map( 59 | fun(Expire) -> 60 | Ref = ets:new(undefined, [Type, protected]), 61 | {T + Expire, Ref} 62 | end, 63 | lists:seq(TTL, N * TTL, TTL) 64 | ), 65 | Heap#heap{segments = queue:from_list(Segments)}. 66 | 67 | %% 68 | %% 69 | -spec refs(#heap{}) -> [segment()]. 70 | 71 | refs(#heap{segments = Segments}) -> 72 | lists:reverse(queue:to_list(Segments)). 73 | 74 | %% 75 | %% split heap to writable segment and others 76 | -spec split(#heap{}) -> {segment(), queue()}. 77 | 78 | split(#heap{segments = Segments}) -> 79 | {{value, Head}, Tail} = queue:out_r(Segments), 80 | {Head, Tail}. 81 | 82 | 83 | -spec split(_, #heap{}) -> {segment(), queue()}. 84 | 85 | split(Expire, #heap{segments = {Tail, Head}} = Heap) -> 86 | case 87 | lists:splitwith( 88 | fun({T, _}) -> T > Expire end, 89 | Tail 90 | ) 91 | of 92 | {_, []} -> 93 | case 94 | lists:splitwith( 95 | fun({T, _}) -> T < Expire end, 96 | Head 97 | ) 98 | of 99 | {[Segment | A], []} -> 100 | {Segment, {Tail, A}}; 101 | {A, [Segment | B]} -> 102 | {Segment, {Tail, A ++ B}} 103 | end; 104 | {[], _} -> 105 | split(Heap); 106 | {A, B} -> 107 | [Segment | Ax] = lists:reverse(A), 108 | {Segment, {lists:reverse(Ax) ++ B, Head}} 109 | end. 110 | 111 | 112 | %% 113 | %% slip heap segments and report reason 114 | -spec slip(heir(), #heap{}) -> {ok | ttl | oom | ooc , #heap{}}. 115 | 116 | slip(Heir, #heap{} = Heap) -> 117 | case is_expired(cache_util:now(), Heap) of 118 | false -> 119 | {ok, Heap}; 120 | Reason -> 121 | {Reason, 122 | heap_remove_segment(Heir, heap_create_segment(Heap)) 123 | } 124 | end. 125 | 126 | is_expired(Time, Heap) -> 127 | case is_expired_tail(Time, Heap) of 128 | false -> 129 | is_expired_head(Heap); 130 | Return -> 131 | Return 132 | end. 133 | 134 | is_expired_tail(Time, #heap{segments = Segments}) -> 135 | {Expire, _} = queue:head(Segments), 136 | case Time >= Expire of 137 | true -> 138 | ttl; 139 | false -> 140 | false 141 | end. 142 | 143 | is_expired_head(#heap{cardinality = Card, memory = Mem, segments = Segments}) -> 144 | {_, Ref} = queue:last(Segments), 145 | case 146 | {ets:info(Ref, size) >= Card, ets:info(Ref, memory) >= Mem} 147 | of 148 | {true, _} -> ooc; 149 | {_, true} -> oom; 150 | _ -> false 151 | end. 152 | 153 | heap_create_segment(#heap{type = Type, ttl = TTL, segments = Segments} = Heap) -> 154 | {LastTTL, _} = queue:last(Segments), 155 | Ref = ets:new(undefined, [Type, protected]), 156 | Expire = TTL + LastTTL, 157 | Heap#heap{segments = queue:in({Expire, Ref}, Segments)}. 158 | 159 | heap_remove_segment(Heir, #heap{segments = Segments} = Heap) -> 160 | {{value, {_, Ref}}, T} = queue:out(Segments), 161 | true = free(Heir, Ref), 162 | Heap#heap{segments = T}. 163 | 164 | %% 165 | %% purge cache segments 166 | -spec purge(heir(), #heap{}) -> #heap{}. 167 | 168 | purge(Heir, #heap{segments = Segments} = Heap) -> 169 | lists:foreach( 170 | fun({_, Ref}) -> true = free(Heir, Ref) end, 171 | queue:to_list(Segments) 172 | ), 173 | init(Heap#heap{segments = undefined}). 174 | 175 | %% 176 | %% destroy heap segment 177 | free(undefined, Ref) -> 178 | ets:delete(Ref); 179 | 180 | free(Heir, Ref) 181 | when is_pid(Heir) -> 182 | case erlang:is_process_alive(Heir) of 183 | true -> 184 | ets:give_away(Ref, Heir, evicted); 185 | false -> 186 | ets:delete(Ref) 187 | end; 188 | 189 | free(Heir, Ref) 190 | when is_atom(Heir) -> 191 | case erlang:whereis(Heir) of 192 | undefined -> 193 | ets:delete(Ref); 194 | Pid -> 195 | ets:give_away(Ref, Pid, evicted) 196 | end. 197 | 198 | 199 | -------------------------------------------------------------------------------- /src/cache_shards.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Licensed under the Apache License, Version 2.0 (the "License"); 3 | %% you may not use this file except in compliance with the License. 4 | %% You may obtain a copy of the License at 5 | %% 6 | %% http://www.apache.org/licenses/LICENSE-2.0 7 | %% 8 | %% Unless required by applicable law or agreed to in writing, software 9 | %% distributed under the License is distributed on an "AS IS" BASIS, 10 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | %% See the License for the specific language governing permissions and 12 | %% limitations under the License. 13 | %% 14 | -module(cache_shards). 15 | 16 | -export([ 17 | start_link/2, 18 | start_link/3, 19 | start/2, 20 | start/3, 21 | drop/1, 22 | get_shard/2, 23 | get/2, 24 | put/3, 25 | delete/2, 26 | get_stat/1 27 | ]). 28 | 29 | -export_type([stat/0]). 30 | 31 | -define(default_app, cache). 32 | -define(app_env_key, cache_shards). 33 | 34 | -type stat() :: #{atom() => integer()}. 35 | 36 | %% Public API 37 | 38 | -spec start_link(atom(), pos_integer()) -> {ok, pid()} | {error, _}. 39 | start_link(Name, NumShards) -> 40 | start_link(Name, NumShards, []). 41 | 42 | -spec start_link(atom(), pos_integer(), proplists:proplist()) -> {ok, pid()} | {error, _}. 43 | start_link(Name, NumShards, CacheOpts) -> 44 | cache_shards_sup:start_link(Name, NumShards, CacheOpts). 45 | 46 | -spec start(atom(), pos_integer()) -> {ok, pid()} | {error, _}. 47 | start(Name, NumShards) -> 48 | start(Name, NumShards, []). 49 | 50 | -spec start(atom(), pos_integer(), proplists:proplist()) -> {ok, pid()} | {error, _}. 51 | start(Name, NumShards, CacheOpts) -> 52 | case cache_shards_sup:start_link(Name, NumShards, CacheOpts) of 53 | {ok, Pid} -> 54 | unlink(Pid), 55 | {ok, Pid}; 56 | {error, _} = Error -> 57 | Error 58 | end. 59 | 60 | -spec drop(atom()) -> ok | {error, invalid_cache}. 61 | drop(Name) -> 62 | cache_shards_sup:free(Name). 63 | 64 | 65 | -spec get_shard(atom(), term()) -> {ok, atom()} | {error, invalid_cache}. 66 | get_shard(Name, Key) -> 67 | CacheShards = application:get_env(?default_app, ?app_env_key, #{}), 68 | case maps:find(Name, CacheShards) of 69 | {ok, NumShards} -> 70 | ID = erlang:phash2(Key) rem NumShards + 1, 71 | {ok, make_shard_name(Name, ID)}; 72 | error -> 73 | {error, invalid_cache} 74 | end. 75 | 76 | 77 | -spec get(atom(), term()) -> {ok, term()} | {error, not_found} | {error, invalid_cache}. 78 | get(Name, Key) -> 79 | case get_shard(Name, Key) of 80 | {ok, ShardName} -> 81 | case cache:get(ShardName, Key) of 82 | undefined -> {error, not_found}; 83 | Value -> {ok, Value} 84 | end; 85 | {error, invalid_cache} = E -> E 86 | end. 87 | 88 | 89 | -spec put(atom(), term(), term()) -> ok | {error, invalid_cache}. 90 | put(Name, Key, Value) -> 91 | case get_shard(Name, Key) of 92 | {ok, ShardName} -> cache:put(ShardName, Key, Value); 93 | {error, invalid_cache} = E -> E 94 | end. 95 | 96 | 97 | -spec delete(atom(), term()) -> ok | {error, invalid_cache}. 98 | delete(Name, Key) -> 99 | case get_shard(Name, Key) of 100 | {ok, ShardName} -> cache:delete(ShardName, Key); 101 | {error, invalid_cache} = E -> E 102 | end. 103 | 104 | 105 | %% Return map #{size => S, memory => N} which aggregates data from all shard and segments. 106 | %% *size* is a number of objects in all ets tables. 107 | %% *memory* is a memory in bytes allocated for all ets tables. 108 | 109 | -spec get_stat(atom()) -> {ok, stat()} | {error, invalid_cache}. 110 | get_stat(Name) -> 111 | CacheShards = application:get_env(?default_app, ?app_env_key, #{}), 112 | case maps:find(Name, CacheShards) of 113 | {ok, NumShards} -> 114 | Info = do_get_stat(Name, NumShards), 115 | {ok, Info}; 116 | error -> {error, invalid_cache} 117 | end. 118 | 119 | 120 | %% Inner functions 121 | 122 | -spec make_shard_name(atom(), pos_integer()) -> atom(). 123 | make_shard_name(Name, ID) -> 124 | list_to_existing_atom(atom_to_list(Name) ++ "_" ++ integer_to_list(ID)). 125 | 126 | -spec do_get_stat(atom(), pos_integer()) -> stat(). 127 | do_get_stat(Name, NumShards) -> 128 | WordSize = erlang:system_info(wordsize), 129 | lists:foldl( 130 | fun(ID, #{size := Size, memory := Memory} = Acc) -> 131 | ShardName = make_shard_name(Name, ID), 132 | Info = cache:i(ShardName), 133 | SegmentSizes = proplists:get_value(size, Info), 134 | SegmentMemory = proplists:get_value(memory, Info), 135 | Acc#{ 136 | size => Size + lists:sum(SegmentSizes), 137 | memory => Memory + lists:sum(SegmentMemory) * WordSize 138 | } 139 | end, 140 | #{size => 0, memory => 0}, 141 | lists:seq(1, NumShards) 142 | ). 143 | -------------------------------------------------------------------------------- /src/cache_shards_sup.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Licensed under the Apache License, Version 2.0 (the "License"); 3 | %% you may not use this file except in compliance with the License. 4 | %% You may obtain a copy of the License at 5 | %% 6 | %% http://www.apache.org/licenses/LICENSE-2.0 7 | %% 8 | %% Unless required by applicable law or agreed to in writing, software 9 | %% distributed under the License is distributed on an "AS IS" BASIS, 10 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | %% See the License for the specific language governing permissions and 12 | %% limitations under the License. 13 | %% 14 | -module(cache_shards_sup). 15 | -behaviour(supervisor). 16 | 17 | -export([start_link/3, init/1, free/1]). 18 | 19 | %% 20 | -define(CHILD(Type, I), {I, {I, start_link, []}, permanent, 5000, Type, dynamic}). 21 | -define(CHILD(Type, I, Args), {I, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 22 | -define(CHILD(Type, ID, I, Args), {ID, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 23 | 24 | %% 25 | %% 26 | start_link(Name, NumShards, Opts) -> 27 | case supervisor:start_link({local, Name}, ?MODULE, [Name, NumShards, Opts]) of 28 | {ok, Sup} -> 29 | ok = register_cache_shards(Name, NumShards), 30 | {ok, Sup}; 31 | {error, _} = Error -> 32 | Error 33 | end. 34 | 35 | free(Name) -> 36 | case erlang:whereis(Name) of 37 | undefined -> 38 | {error, invalid_cache}; 39 | Pid -> 40 | ok = deregister_cache_shards(Name), 41 | Ref = monitor(process, Pid), 42 | exit(Pid, shutdown), 43 | receive 44 | {'DOWN', Ref, process, Pid, _Reason} -> 45 | ok 46 | after 30000 -> 47 | error(exit_timeout) 48 | end 49 | end. 50 | 51 | init([Name, NumShards, Opts]) -> 52 | {ok, 53 | { 54 | {one_for_one, 4, 1800}, 55 | [shard(make_shard_name(Name, Id), Opts) || Id <- lists:seq(1, NumShards)] 56 | } 57 | }. 58 | 59 | shard(ShardName, CacheOpts) -> 60 | #{ 61 | id => ShardName, 62 | start => {cache, start_link, [ShardName, CacheOpts]}, 63 | restart => permanent, 64 | shutdown => 2000, 65 | type => worker, 66 | modules => [cache] 67 | }. 68 | 69 | make_shard_name(Name, ID) -> 70 | list_to_atom(atom_to_list(Name) ++ "_" ++ integer_to_list(ID)). 71 | 72 | register_cache_shards(Name, NumShards) -> 73 | CacheShards1 = application:get_env(cache, cache_shards, #{}), 74 | CacheShards2 = CacheShards1#{Name => NumShards}, 75 | application:set_env(cache, cache_shards, CacheShards2). 76 | 77 | deregister_cache_shards(Name) -> 78 | CacheShards1 = application:get_env(cache, cache_shards, #{}), 79 | CacheShards2 = maps:remove(Name, CacheShards1), 80 | application:set_env(cache, cache_shards, CacheShards2). 81 | -------------------------------------------------------------------------------- /src/cache_sup.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% root cache supervisor 18 | -module(cache_sup). 19 | -behaviour(supervisor). 20 | -author('Dmitry Kolesnikov '). 21 | -author('Jose Luis Navarro '). 22 | 23 | %% API 24 | -export([start_link/0]). 25 | 26 | %% Supervisor callbacks 27 | -export([init/1]). 28 | 29 | %% 30 | -define(CHILD(Type, I), {I, {I, start_link, []}, permanent, 5000, Type, dynamic}). 31 | -define(CHILD(Type, I, Args), {I, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 32 | -define(CHILD(Type, ID, I, Args), {ID, {I, start_link, Args}, permanent, 5000, Type, dynamic}). 33 | 34 | %% =================================================================== 35 | %% API functions 36 | %% =================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | %% =================================================================== 42 | %% Supervisor callbacks 43 | %% =================================================================== 44 | 45 | init([]) -> 46 | {ok, 47 | { 48 | {one_for_one, 4, 1800}, 49 | [ 50 | ?CHILD(supervisor, cache_bucket_sup) 51 | ] 52 | } 53 | }. 54 | 55 | -------------------------------------------------------------------------------- /src/cache_util.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2012 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | %% @description 17 | %% cache utility 18 | -module(cache_util). 19 | 20 | -export([ 21 | mdiv/2, madd/2, mmul/2, now/0, stats/2, stats/3 22 | ]). 23 | 24 | %% 25 | %% maybe div 26 | mdiv(X, Y) 27 | when X =:= undefined orelse Y =:= undefined -> 28 | undefined; 29 | mdiv(X, Y) -> 30 | X div Y. 31 | 32 | %% 33 | %% maybe add 34 | madd(X, Y) 35 | when X =:= undefined orelse Y =:= undefined -> 36 | undefined; 37 | madd(X, Y) -> 38 | X + Y. 39 | 40 | %% 41 | %% maybe multiply 42 | mmul(X, Y) 43 | when X =:= undefined orelse Y =:= undefined -> 44 | undefined; 45 | mmul(X, Y) -> 46 | X * Y. 47 | 48 | %% 49 | %% 50 | now() -> 51 | {Mega, Sec, _} = os:timestamp(), 52 | Mega * 1000000 + Sec. 53 | 54 | %% 55 | %% 56 | stats(undefined, _) -> 57 | ok; 58 | stats({M, F}, Counter) -> 59 | M:F(Counter); 60 | stats(Fun, Counter) 61 | when is_function(Fun) -> 62 | Fun(Counter). 63 | 64 | stats(undefined, _, _) -> 65 | ok; 66 | stats({M, F}, Counter, Val) -> 67 | M:F(Counter, Val); 68 | stats(Fun, Counter, Val) 69 | when is_function(Fun) -> 70 | Fun(Counter, Val). 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/cache_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2015 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | -module(cache_SUITE). 17 | -include_lib("common_test/include/ct.hrl"). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | all() -> 23 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 24 | Test =/= module_info, 25 | Test =/= init_per_suite, 26 | Test =/= end_per_suite, 27 | NAry =:= 1 28 | ]. 29 | 30 | %%%---------------------------------------------------------------------------- 31 | %%% 32 | %%% cache primitives 33 | %%% 34 | %%%---------------------------------------------------------------------------- 35 | 36 | lifecycle(_Config) -> 37 | {ok, Cache} = cache:start_link([]), 38 | ok = cache:drop(Cache). 39 | 40 | i(_Config) -> 41 | {ok, Cache} = cache:start_link([]), 42 | Spec = cache:i(Cache), 43 | {heap, _} = lists:keyfind(heap, 1, Spec), 44 | {expire, _} = lists:keyfind(expire, 1, Spec), 45 | {size, _} = lists:keyfind(size, 1, Spec), 46 | {memory, _} = lists:keyfind(memory, 1, Spec), 47 | ok = cache:drop(Cache). 48 | 49 | heap(_Config) -> 50 | {ok, Cache} = cache:start_link([]), 51 | ok = cache:put(Cache, key, val), 52 | [{key, val}] = ets:lookup(cache:heap(Cache, 1), key), 53 | ok = cache:drop(Cache). 54 | 55 | heap_recover_failure(_Config) -> 56 | {ok, Cache} = cache:start_link([]), 57 | ok = cache:put(Cache, key, val), 58 | cache:heap(Cache, 10000), 59 | [{key, val}] = ets:lookup(cache:heap(Cache, 1), key), 60 | ok = cache:drop(Cache). 61 | 62 | purge(_Config) -> 63 | {ok, Cache} = cache:start_link([]), 64 | ok = cache:put(Cache, key, val), 65 | val = cache:get(Cache, key), 66 | cache:purge(Cache), 67 | undefined = cache:get(Cache, key), 68 | ok = cache:drop(Cache). 69 | 70 | evict_ttl(_Config) -> 71 | {ok, Cache} = cache:start_link([{n, 10}, {ttl, 10}]), 72 | ok = cache:put(Cache, key, val, 3), 73 | timer:sleep(1200), 74 | val = cache:lookup(Cache, key), 75 | timer:sleep(2200), 76 | undefined = cache:lookup(Cache, key), 77 | ok = cache:drop(Cache). 78 | 79 | evict_no_ttl(_Config) -> 80 | {ok, Cache} = cache:start_link([{n, 3}, {ttl, 3}]), 81 | ok = cache:put(Cache, key, val), 82 | timer:sleep(1200), 83 | val = cache:lookup(Cache, key), 84 | timer:sleep(2200), 85 | undefined = cache:lookup(Cache, key), 86 | ok = cache:drop(Cache). 87 | 88 | evict_ooc(_Config) -> 89 | {ok, Cache} = cache:start_link([{n, 10}, {size, 20}, {check, 1}]), 90 | ok = cache:put(Cache, key1, val), 91 | ok = cache:put(Cache, key2, val), 92 | [2 | _] = cache:i(Cache, size), 93 | timer:sleep(1200), 94 | [0, 2 | _] = cache:i(Cache, size), 95 | ok = cache:drop(Cache). 96 | 97 | %%%---------------------------------------------------------------------------- 98 | %%% 99 | %%% cache basic i/o 100 | %%% 101 | %%%---------------------------------------------------------------------------- 102 | 103 | put(_Config) -> 104 | {ok, Cache} = cache:start_link([]), 105 | ok = cache:put(Cache, key, val), 106 | [{key, val}] = ets:lookup(cache:heap(Cache, 1), key), 107 | ok = cache:drop(Cache). 108 | 109 | put_(_Config) -> 110 | {ok, Cache} = cache:start_link([]), 111 | ok = cache:put_(Cache, key, val), 112 | val = cache:get(Cache, key), 113 | ok = cache:drop(Cache). 114 | 115 | 116 | get(_Config) -> 117 | {ok, Cache} = cache:start_link([{n, 10}, {ttl, 10}]), 118 | ok = cache:put(Cache, key1, val), 119 | val = cache:get(Cache, key1), 120 | ok = cache:put(Cache, key2, val, 5), 121 | val = cache:get(Cache, key2), 122 | undefined = cache:get(Cache, unknown), 123 | ok = cache:drop(Cache). 124 | 125 | lookup(_Config) -> 126 | {ok, Cache} = cache:start_link([]), 127 | ok = cache:put(Cache, key, val), 128 | val = cache:lookup(Cache, key), 129 | undefined = cache:lookup(Cache, unknown), 130 | ok = cache:drop(Cache). 131 | 132 | has(_Config) -> 133 | {ok, Cache} = cache:start_link([]), 134 | ok = cache:put(Cache, key, val), 135 | true = cache:has(Cache, key), 136 | false = cache:has(Cache, unknown), 137 | ok = cache:drop(Cache). 138 | 139 | ttl(_Config) -> 140 | {ok, Cache} = cache:start_link([{n,10}, {ttl, 60}]), 141 | ok = cache:put(Cache, key1, val), 142 | true = cache:ttl(Cache, key1) > 55, 143 | ok = cache:put(Cache, key2, val, 10), 144 | true = cache:ttl(Cache, key2) > 9, 145 | undefined = cache:ttl(Cache, unknown), 146 | ok = cache:drop(Cache). 147 | 148 | remove(_Config) -> 149 | {ok, Cache} = cache:start_link([]), 150 | ok = cache:put(Cache, key, val), 151 | true = cache:has(Cache, key), 152 | ok = cache:remove(Cache, key), 153 | false= cache:has(Cache, key), 154 | ok = cache:drop(Cache). 155 | 156 | remove_(_Config) -> 157 | {ok, Cache} = cache:start_link([]), 158 | ok = cache:put(Cache, key, val), 159 | true = cache:has(Cache, key), 160 | ok = cache:remove_(Cache, key), 161 | false= cache:has(Cache, key), 162 | ok = cache:drop(Cache). 163 | 164 | apply(_Config) -> 165 | {ok, Cache} = cache:start_link([]), 166 | val = cache:apply(Cache, key, fun(undefined) -> val end), 167 | val = cache:get(Cache, key), 168 | 169 | lav = cache:apply(Cache, key, fun(val) -> lav end), 170 | lav = cache:get(Cache, key), 171 | 172 | lav = cache:apply(Cache, key, fun(X) -> X end), 173 | lav = cache:get(Cache, key), 174 | 175 | undefined = cache:apply(Cache, key, fun(_) -> undefined end), 176 | lav = cache:get(Cache, key), 177 | 178 | ok = cache:drop(Cache). 179 | 180 | apply_(_Config) -> 181 | {ok, Cache} = cache:start_link([]), 182 | cache:apply_(Cache, key, fun(undefined) -> val end), 183 | val = cache:get(Cache, key), 184 | 185 | cache:apply_(Cache, key, fun(val) -> lav end), 186 | lav = cache:get(Cache, key), 187 | 188 | cache:apply_(Cache, key, fun(X) -> X end), 189 | lav = cache:get(Cache, key), 190 | 191 | cache:apply_(Cache, key, fun(_) -> undefined end), 192 | lav = cache:get(Cache, key), 193 | 194 | ok = cache:drop(Cache). 195 | 196 | %%%---------------------------------------------------------------------------- 197 | %%% 198 | %%% cache extended i/o 199 | %%% 200 | %%%---------------------------------------------------------------------------- 201 | 202 | acc(_Config) -> 203 | {ok, Cache} = cache:start_link([]), 204 | undefined = cache:acc(Cache, key, 1), 205 | 1 = cache:acc(Cache, key, 10), 206 | 11 = cache:acc(Cache, key, 1), 207 | ok = cache:drop(Cache). 208 | 209 | acc_(_Config) -> 210 | {ok, Cache} = cache:start_link([]), 211 | cache:acc_(Cache, key, 1), 212 | cache:acc_(Cache, key, 10), 213 | 11 = cache:get(Cache, key), 214 | ok = cache:drop(Cache). 215 | 216 | set(_Config) -> 217 | {ok, Cache} = cache:start_link([]), 218 | ok = cache:set(Cache, key, val), 219 | val = cache:get(Cache, key), 220 | ok = cache:drop(Cache). 221 | 222 | add(_Config) -> 223 | {ok, Cache} = cache:start_link([]), 224 | ok = cache:add(Cache, key, val), 225 | {error, conflict} = cache:add(Cache, key, val), 226 | ok = cache:drop(Cache). 227 | 228 | add_(_Config) -> 229 | {ok, Cache} = cache:start_link([]), 230 | cache:add_(Cache, key, val), 231 | cache:add_(Cache, key, non), 232 | val = cache:get(Cache, key), 233 | ok = cache:drop(Cache). 234 | 235 | replace(_Config) -> 236 | {ok, Cache} = cache:start_link([]), 237 | {error, not_found} = cache:replace(Cache, key, val), 238 | ok = cache:set(Cache, key, val), 239 | ok = cache:replace(Cache, key, val), 240 | ok = cache:drop(Cache). 241 | 242 | replace_(_Config) -> 243 | {ok, Cache} = cache:start_link([]), 244 | cache:replace_(Cache, key, non), 245 | undefined = cache:get(Cache, key), 246 | ok = cache:set(Cache, key, val), 247 | cache:replace_(Cache, key, foo), 248 | foo = cache:get(Cache, key), 249 | ok = cache:drop(Cache). 250 | 251 | append(_Config) -> 252 | {ok, Cache} = cache:start_link([]), 253 | ok = cache:append(Cache, key, a), 254 | [a] = cache:get(Cache, key), 255 | ok = cache:append(Cache, key, b), 256 | [a, b] = cache:get(Cache, key), 257 | ok = cache:append(Cache, key, c), 258 | [a, b, c] = cache:get(Cache, key), 259 | ok = cache:drop(Cache). 260 | 261 | append_(_Config) -> 262 | {ok, Cache} = cache:start_link([]), 263 | ok = cache:append_(Cache, key, a), 264 | ok = cache:append_(Cache, key, b), 265 | ok = cache:append_(Cache, key, c), 266 | [a, b, c] = cache:get(Cache, key), 267 | ok = cache:drop(Cache). 268 | 269 | prepend(_Config) -> 270 | {ok, Cache} = cache:start_link([]), 271 | ok = cache:prepend(Cache, key, a), 272 | [a] = cache:get(Cache, key), 273 | ok = cache:prepend(Cache, key, b), 274 | [b, a] = cache:get(Cache, key), 275 | ok = cache:prepend(Cache, key, c), 276 | [c, b, a] = cache:get(Cache, key), 277 | ok = cache:drop(Cache). 278 | 279 | prepend_(_Config) -> 280 | {ok, Cache} = cache:start_link([]), 281 | ok = cache:prepend_(Cache, key, a), 282 | ok = cache:prepend_(Cache, key, b), 283 | ok = cache:prepend_(Cache, key, c), 284 | [c, b, a] = cache:get(Cache, key), 285 | ok = cache:drop(Cache). 286 | 287 | delete(_Config) -> 288 | {ok, Cache} = cache:start_link([]), 289 | ok = cache:put(Cache, key, val), 290 | true = cache:has(Cache, key), 291 | ok = cache:delete(Cache, key), 292 | false= cache:has(Cache, key), 293 | ok = cache:drop(Cache). 294 | 295 | 296 | -------------------------------------------------------------------------------- /test/cache_heap_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright 2015 Dmitry Kolesnikov, All Rights Reserved 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %% 16 | -module(cache_heap_SUITE). 17 | -include_lib("common_test/include/ct.hrl"). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | all() -> 23 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 24 | Test =/= module_info, 25 | Test =/= init_per_suite, 26 | Test =/= end_per_suite, 27 | NAry =:= 1 28 | ]. 29 | 30 | init_per_testcase(_, Config) -> 31 | meck:new(cache_util, [passthrough]), 32 | meck:expect(cache_util, now, fun() -> 0 end), 33 | Config. 34 | 35 | end_per_testcase(_, _) -> 36 | meck:unload(cache_util). 37 | 38 | %% 39 | heap_init(_) -> 40 | {heap, set, 10, 6, 100, 1280, Segments} = cache_heap:new(set, 10, 60, 1000, 102400), 41 | Expect = lists:seq(6, 60, 6), 42 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 43 | 44 | 45 | %% 46 | heap_purge(_) -> 47 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 48 | {heap, set, 10, 6, 100, 1280, Segments} = cache_heap:purge(undefined, Heap), 49 | Expect = lists:seq(6, 60, 6), 50 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 51 | 52 | %% 53 | heap_slip_ok(_) -> 54 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 55 | {ok, Heap} = cache_heap:slip(undefined, Heap). 56 | 57 | %% 58 | heap_slip_ttl(_) -> 59 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 60 | meck:expect(cache_util, now, fun() -> 6 end), 61 | {ttl, 62 | {heap, set, 10, 6, 100, 1280, Segments} 63 | } = cache_heap:slip(undefined, Heap), 64 | Expect = lists:seq(12, 66, 6), 65 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 66 | 67 | %% 68 | heap_split_last(_) -> 69 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 70 | {{60, _}, Segments} = cache_heap:split(Heap), 71 | Expect = lists:seq(6, 54, 6), 72 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 73 | 74 | %% 75 | heap_split_45(_) -> 76 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 77 | {{48, _}, Segments} = cache_heap:split(45, Heap), 78 | Expect = lists:seq(6, 42, 6) ++ lists:seq(54, 60, 6), 79 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 80 | 81 | %% 82 | heap_split_15(_) -> 83 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 84 | {{18, _}, Segments} = cache_heap:split(15, Heap), 85 | Expect = lists:seq(6, 12, 6) ++ lists:seq(24, 60, 6), 86 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 87 | 88 | %% 89 | heap_split_65(_) -> 90 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 91 | {{60, _}, Segments} = cache_heap:split(65, Heap), 92 | Expect = lists:seq(6, 54, 6), 93 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 94 | 95 | %% 96 | heap_split_0(_) -> 97 | Heap = cache_heap:new(set, 10, 60, 1000, 102400), 98 | {{6, _}, Segments} = cache_heap:split(0, Heap), 99 | Expect = lists:seq(12, 60, 6), 100 | Expect = [Expire || {Expire, _} <- queue:to_list(Segments)]. 101 | 102 | 103 | -------------------------------------------------------------------------------- /test/cache_shards_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Licensed under the Apache License, Version 2.0 (the "License"); 3 | %% you may not use this file except in compliance with the License. 4 | %% You may obtain a copy of the License at 5 | %% 6 | %% http://www.apache.org/licenses/LICENSE-2.0 7 | %% 8 | %% Unless required by applicable law or agreed to in writing, software 9 | %% distributed under the License is distributed on an "AS IS" BASIS, 10 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | %% See the License for the specific language governing permissions and 12 | %% limitations under the License. 13 | %% 14 | -module(cache_shards_SUITE). 15 | 16 | -include_lib("common_test/include/ct.hrl"). 17 | -include_lib("eunit/include/eunit.hrl"). 18 | 19 | -compile([export_all]). 20 | -compile(nowarn_export_all). 21 | 22 | all() -> 23 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 24 | Test =/= module_info, 25 | Test =/= init_per_suite, 26 | Test =/= end_per_suite, 27 | NAry =:= 1 28 | ]. 29 | 30 | 31 | init_per_suite(Config) -> 32 | ok = application:start(cache), 33 | Config. 34 | 35 | end_per_suite(_Config) -> 36 | ok. 37 | 38 | 39 | init_per_testcase(TestCase, Config) -> 40 | CacheName = list_to_atom("cache_" ++ atom_to_list(TestCase)), 41 | [{cache_name, CacheName} | Config]. 42 | 43 | end_per_testcase(_TestCase, Config) -> 44 | proplists:delete(cache_name, Config). 45 | 46 | %% 47 | %% 48 | lifecycle_sharded_cache(_Config) -> 49 | ?assert(is_pid(whereis(cache_sup))), 50 | 51 | ?assertMatch({ok, _}, cache_shards:start(cache1, 4)), 52 | ?assertMatch({error, {already_started, _}}, cache_shards:start(cache1, 4)), 53 | 54 | ?assertMatch({ok, _}, cache_shards:start(cache2, 8)), 55 | ?assertMatch({error, {already_started, _}}, cache_shards:start(cache2, 8)), 56 | 57 | {ok, CacheShards1} = application:get_env(cache, cache_shards), 58 | ?assertEqual(2, maps:size(CacheShards1)), 59 | ?assertMatch(#{cache1 := 4, cache2 := 8}, CacheShards1), 60 | 61 | ?assertEqual(ok, cache_shards:drop(cache1)), 62 | ?assertEqual({error, invalid_cache}, cache_shards:drop(cache1)), 63 | 64 | ?assertEqual(ok, cache_shards:drop(cache2)), 65 | ?assertEqual({error, invalid_cache}, cache_shards:drop(cache2)), 66 | ?assertEqual({error, invalid_cache}, cache_shards:drop(some_invalid_cache_name)), 67 | 68 | {ok, CacheShards2} = application:get_env(cache, cache_shards), 69 | ?assertEqual(0, maps:size(CacheShards2)), 70 | ?assertMatch(#{}, CacheShards2), 71 | ok. 72 | 73 | 74 | get_shard(Config) -> 75 | CacheName = ?config(cache_name, Config), 76 | {ok, _} = cache_shards:start(CacheName, 4), 77 | Shards = [cache_get_shard_1, cache_get_shard_2, cache_get_shard_3, cache_get_shard_4], 78 | lists:foreach( 79 | fun(ID) -> 80 | {ok, Shard} = cache_shards:get_shard(CacheName, ID), 81 | ?assert(lists:member(Shard, Shards)) 82 | end, 83 | lists:seq(1, 100) 84 | ), 85 | ?assertEqual({error, invalid_cache}, cache_shards:get_shard(some_invalid_cache_name, 1)), 86 | ok = cache_shards:drop(CacheName), 87 | ok. 88 | 89 | 90 | get_put_delete(Config) -> 91 | CacheName = ?config(cache_name, Config), 92 | {ok, _} = cache_shards:start(CacheName, 4), 93 | 94 | ?assertEqual({error, not_found}, cache_shards:get(CacheName, key1)), 95 | ?assertEqual(ok, cache_shards:put(CacheName, key1, value1)), 96 | ?assertEqual({ok, value1}, cache_shards:get(CacheName, key1)), 97 | ?assertEqual({error, invalid_cache}, cache_shards:get(some_invalid_cache_name, key1)), 98 | 99 | {ok, Shard} = cache_shards:get_shard(CacheName, key1), 100 | ?assertEqual(value1, cache:get(Shard, key1)), 101 | 102 | ?assertEqual(ok, cache_shards:delete(CacheName, key1)), 103 | ?assertEqual({error, not_found}, cache_shards:get(CacheName, key1)), 104 | 105 | ok = cache_shards:drop(CacheName), 106 | ok. 107 | -------------------------------------------------------------------------------- /test/cover.spec: -------------------------------------------------------------------------------- 1 | {incl_app, cache, details}. 2 | -------------------------------------------------------------------------------- /test/tests.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% logs 3 | {logdir, "/tmp/test/cache/"}. 4 | 5 | %% 6 | %% suites 7 | {suites, ".", all}. 8 | 9 | %% 10 | %% code coverage 11 | {cover, "cover.spec"}. 12 | --------------------------------------------------------------------------------