stockpile_cache 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +35 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +56 -0
- data/LICENSE.txt +201 -0
- data/README.md +135 -0
- data/Rakefile +8 -0
- data/bin/console +12 -0
- data/bin/setup +8 -0
- data/lib/stockpile.rb +115 -0
- data/lib/stockpile/cache.rb +42 -0
- data/lib/stockpile/cached_value_reader.rb +33 -0
- data/lib/stockpile/configuration.rb +79 -0
- data/lib/stockpile/constants.rb +27 -0
- data/lib/stockpile/executor.rb +75 -0
- data/lib/stockpile/failed_lock_execution.rb +22 -0
- data/lib/stockpile/lock.rb +62 -0
- data/lib/stockpile/locked_execution_result.rb +37 -0
- data/lib/stockpile/redis_connection.rb +57 -0
- data/lib/stockpile_cache.rb +3 -0
- data/stockpile-cache.gemspec +28 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f846deab3025ca6771b8cf40a4ff55c358256164208d3dd9409667efc51aa013
|
|
4
|
+
data.tar.gz: 0d9dbe79e95be12dbbb6db1edc7f0c3e2d9d40cc9731c61362d5f415a81aaec9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c7921d065ed50ff8fab43453a2d5532d724939e55dea2f98df06e8a92ef1bbfb7b513fb87ffcf5841ac584a4a7b789220ae06ffb8e30ae6d33ef4c2469457da5
|
|
7
|
+
data.tar.gz: a61bc77c28a775e309b6f63f9af18891e54e0027e745ca58cb68fe98524cd931bc5fe1a7087bd479f8d8c1b9e557cc7797ccf309394415d83a25af589589d307
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
jobs:
|
|
3
|
+
build:
|
|
4
|
+
docker:
|
|
5
|
+
- image: circleci/ruby:2.6.4
|
|
6
|
+
- image: circleci/redis
|
|
7
|
+
|
|
8
|
+
working_directory: ~/repo
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- checkout
|
|
12
|
+
|
|
13
|
+
- restore_cache:
|
|
14
|
+
keys:
|
|
15
|
+
- bundle-{{ checksum "Gemfile.lock" }}
|
|
16
|
+
|
|
17
|
+
- run:
|
|
18
|
+
name: install dependencies
|
|
19
|
+
command: |
|
|
20
|
+
gem update --system
|
|
21
|
+
gem install bundler
|
|
22
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
|
23
|
+
|
|
24
|
+
- save_cache:
|
|
25
|
+
key: bundle-{{ checksum "Gemfile.lock" }}
|
|
26
|
+
paths:
|
|
27
|
+
- ./vendor/bundle
|
|
28
|
+
|
|
29
|
+
- run:
|
|
30
|
+
name: rubocop
|
|
31
|
+
command: bundle exec rubocop
|
|
32
|
+
|
|
33
|
+
- run:
|
|
34
|
+
name: rspec
|
|
35
|
+
command: bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at engineering@convertkit.com. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
stockpile_cache (1.0.0)
|
|
5
|
+
connection_pool
|
|
6
|
+
oj
|
|
7
|
+
rake
|
|
8
|
+
redis
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
ast (2.4.0)
|
|
14
|
+
connection_pool (2.2.2)
|
|
15
|
+
diff-lcs (1.3)
|
|
16
|
+
jaro_winkler (1.5.3)
|
|
17
|
+
oj (3.9.1)
|
|
18
|
+
parallel (1.17.0)
|
|
19
|
+
parser (2.6.4.1)
|
|
20
|
+
ast (~> 2.4.0)
|
|
21
|
+
rainbow (3.0.0)
|
|
22
|
+
rake (12.3.3)
|
|
23
|
+
redis (4.1.2)
|
|
24
|
+
rspec (3.8.0)
|
|
25
|
+
rspec-core (~> 3.8.0)
|
|
26
|
+
rspec-expectations (~> 3.8.0)
|
|
27
|
+
rspec-mocks (~> 3.8.0)
|
|
28
|
+
rspec-core (3.8.2)
|
|
29
|
+
rspec-support (~> 3.8.0)
|
|
30
|
+
rspec-expectations (3.8.4)
|
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
32
|
+
rspec-support (~> 3.8.0)
|
|
33
|
+
rspec-mocks (3.8.1)
|
|
34
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
35
|
+
rspec-support (~> 3.8.0)
|
|
36
|
+
rspec-support (3.8.2)
|
|
37
|
+
rubocop (0.74.0)
|
|
38
|
+
jaro_winkler (~> 1.5.1)
|
|
39
|
+
parallel (~> 1.10)
|
|
40
|
+
parser (>= 2.6)
|
|
41
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
42
|
+
ruby-progressbar (~> 1.7)
|
|
43
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
|
44
|
+
ruby-progressbar (1.10.1)
|
|
45
|
+
unicode-display_width (1.6.0)
|
|
46
|
+
|
|
47
|
+
PLATFORMS
|
|
48
|
+
ruby
|
|
49
|
+
|
|
50
|
+
DEPENDENCIES
|
|
51
|
+
rspec (~> 3.8.0)
|
|
52
|
+
rubocop (~> 0.74.0)
|
|
53
|
+
stockpile_cache!
|
|
54
|
+
|
|
55
|
+
BUNDLED WITH
|
|
56
|
+
2.0.2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
data/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Stockpile [![Build Status][ci-image]][ci] [![Code Climate][codeclimate-image]][codeclimate]
|
|
2
|
+
Stockpile is a simple cache written in Ruby backed by Redis. It has built in
|
|
3
|
+
[cache-stampede](https://en.wikipedia.org/wiki/Cache_stampede) (also known as
|
|
4
|
+
dog-piling) protection.
|
|
5
|
+
|
|
6
|
+
Can be used with any Ruby or Ruby on Rails project. Can be used as a replacement for
|
|
7
|
+
existing Ruby on Rails cache.
|
|
8
|
+
|
|
9
|
+
Intended as a heavy usage cache to prevent concurrent execution of code when
|
|
10
|
+
cache is expired that will lead to congestion collapse of your systems.
|
|
11
|
+
|
|
12
|
+
Upon caching serializes cached value using [Oj](https://github.com/ohler55/oj)
|
|
13
|
+
gem. While reading value from cache will deserialize value from cache using same
|
|
14
|
+
gem.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
When `perform_cached` method is invoked with a key and a block of code as
|
|
18
|
+
arguments Stockpile will attempt to fetch value from cache using given key. If
|
|
19
|
+
no value is returned it will set a lock deferring all other requests for given
|
|
20
|
+
key (for specified amount of time) and run provided block of code and storing
|
|
21
|
+
it's return value at the key. After that a lock will be released allowing other
|
|
22
|
+
requests to fetch their values from cache.
|
|
23
|
+
|
|
24
|
+
In case there is a cache miss and an active execution lock for a given key is
|
|
25
|
+
present request will go into slumber for 2 seconds (configurable by
|
|
26
|
+
`STOCKPILE_SLUMBER` environment variable or by calling `slumber` method on
|
|
27
|
+
configuration object). During slumber request will keep trying to read value
|
|
28
|
+
from cache and if no result is returned during that time cache will be bypassed
|
|
29
|
+
and value will be computed by executing passed in block.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
Add the following line to your Gemfile:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
gem 'stockpile_cache'
|
|
36
|
+
```
|
|
37
|
+
And run `bundle` from your shell.
|
|
38
|
+
|
|
39
|
+
To install gem manually run from your shell:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
gem install stockpile_cache
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
Only requirement to run this gem is [Redis](https://redis.io/). Other than that
|
|
47
|
+
it is not dependant on any other framework or system.
|
|
48
|
+
|
|
49
|
+
## Configration
|
|
50
|
+
The only thing you need to set up is URL of your Redis server. You can do this
|
|
51
|
+
by either setting `STOCKPILE_REDIS_URL` environment variable or by executing
|
|
52
|
+
following code during runtime. For Ruby on Rails create
|
|
53
|
+
`config/initializers/stockpile.rb` file and put the following code in there:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Stockpile.configure do |configuration|
|
|
57
|
+
configuration.redis_url = <REDIS_URL>
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
There are two ways to configure Stockpile: using environment variables or
|
|
62
|
+
invoking configuration block during runtime.
|
|
63
|
+
|
|
64
|
+
Following settings are supported:
|
|
65
|
+
|
|
66
|
+
| Variable | Method | Settings |
|
|
67
|
+
| ------------- | ------------- | ------------- |
|
|
68
|
+
| `STOCKPILE_CONNECTION_POOL` | `connection_pool` | Redis connection pool size to share amongst the fibers or threads in your Ruby. Defaults to `100`. |
|
|
69
|
+
| `STOCKPILE_CONNECTION_TIMEOUT` | `connection_timeout` | How long to wait for a connection from connection pool to become available (in seconds). Defaults to `3`. |
|
|
70
|
+
| `STOCKPILE_LOCK_EXPIRATION` | `lock_expiration` | Time to keep execution lock alive (in seonds). Defaults to `10`. |
|
|
71
|
+
| `STOCKPILE_REDIS_URL` | `redis_url` | URL of your Redis server that will be used for caching. Defaults to `redis://localhost:6379/1`. |
|
|
72
|
+
| `STOCKPILE_REDIS_SENTINELS` | `sentinels` | (optional) Comma separated list of Sentinels IPs for Redis. Defaults to `nil`. Example value: `8.8.8.8:42,8.8.4.4:42`. |
|
|
73
|
+
| `STOCKPILE_SLUMBER` | `slumber` | Timeout (in seconds) for stampede protection lock. After timeout passed in code will be executed instead of reading a value from cache. Defaults to `2`. |
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
To use simply wrap your code into `perform_cached` block:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Stockpile.perform_cached(key: 'meaning_of_life', ttl: 42) do
|
|
80
|
+
21 + 21
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`perform` method accepts 3 named arguments:
|
|
85
|
+
|
|
86
|
+
| Argument | Meaning |
|
|
87
|
+
| ------------- | ------------- |
|
|
88
|
+
| `key` | Pointer in cache by which a value will be either looked up or stored in cache once code provided in block is executed. |
|
|
89
|
+
| `ttl` | (optional) Time in seconds for which a cached value will be stored. Defaults to 300 seconds (5 minutes). |
|
|
90
|
+
| `&block` | Block of code to execute; it's return value will be stored in cache. |
|
|
91
|
+
|
|
92
|
+
## Caveats
|
|
93
|
+
There is no timeout or rescue set for code you will be running through the cache. If
|
|
94
|
+
you need to do either you have to handle it outside of Stockpile.
|
|
95
|
+
|
|
96
|
+
Locks are never set indefinitely and by default will expire after 10 seconds
|
|
97
|
+
allowing next request to trigger cache recalculation. Lock duration is
|
|
98
|
+
configurable by either setting `STOCKPILE_LOCK_EXPIRATION` environment variable
|
|
99
|
+
or by calling `slumber` method on configuration object.
|
|
100
|
+
|
|
101
|
+
While there is an active lock for the key each request trying to read that key
|
|
102
|
+
will wait in slumber for 2 seconds (configurable by `STOCKPILE_SLUMBER`
|
|
103
|
+
environment variable or by calling `slumber` method on configuration object) and
|
|
104
|
+
will bypass cache after that if no value will be set in that time.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
108
|
+
`rspec` to run the tests. You can also run `bin/console` for an interactive
|
|
109
|
+
prompt that will allow you to experiment.
|
|
110
|
+
|
|
111
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
112
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
113
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
114
|
+
git commits and tags, and push the `.gem` file to
|
|
115
|
+
[rubygems.org](https://rubygems.org).
|
|
116
|
+
|
|
117
|
+
## Contributing
|
|
118
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
119
|
+
https://github.com/ConvertKit/stockpile_cache. This project is intended to be a
|
|
120
|
+
safe, welcoming space for collaboration, and contributors are expected to adhere
|
|
121
|
+
to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
The gem is available as open source under the terms of the
|
|
125
|
+
[Apache License Version 2.0] (http://www.apache.org/licenses/LICENSE-2.0).
|
|
126
|
+
|
|
127
|
+
## Code of Conduct
|
|
128
|
+
Everyone interacting in the Stockpile project’s codebases, issue
|
|
129
|
+
trackers, chat rooms and mailing lists is expected to follow the [code of
|
|
130
|
+
conduct](https://github.com/ConvertKit/stockpile_cache/blob/master/CODE_OF_CONDUCT.md).
|
|
131
|
+
|
|
132
|
+
[ci]: https://circleci.com/gh/ConvertKit/stockpile_cache
|
|
133
|
+
[ci-image]: https://circleci.com/gh/ConvertKit/stockpile_cache.svg?style=svg
|
|
134
|
+
[codeclimate]: https://codeclimate.com/github/ConvertKit/stockpile_cache/maintainability
|
|
135
|
+
[codeclimate-image]: https://api.codeclimate.com/v1/badges/f9ca3b6dda3b492b125e/maintainability
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/stockpile.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'connection_pool'
|
|
18
|
+
require 'oj'
|
|
19
|
+
require 'redis'
|
|
20
|
+
require 'timeout'
|
|
21
|
+
|
|
22
|
+
require 'stockpile/constants'
|
|
23
|
+
require 'stockpile/configuration'
|
|
24
|
+
require 'stockpile/redis_connection'
|
|
25
|
+
|
|
26
|
+
require 'stockpile/lock'
|
|
27
|
+
require 'stockpile/locked_execution_result'
|
|
28
|
+
require 'stockpile/failed_lock_execution'
|
|
29
|
+
|
|
30
|
+
require 'stockpile/cache'
|
|
31
|
+
require 'stockpile/cached_value_reader'
|
|
32
|
+
|
|
33
|
+
require 'stockpile/executor'
|
|
34
|
+
|
|
35
|
+
# = Stockpile
|
|
36
|
+
#
|
|
37
|
+
# Simple cache with Redis as a backend and a built in cache-stampede
|
|
38
|
+
# protection. For more information on general usage consider consulting
|
|
39
|
+
# README.md file.
|
|
40
|
+
#
|
|
41
|
+
# While interacting with the cache from within your application
|
|
42
|
+
# avoid re-using anything after :: notation as it is part of internal API
|
|
43
|
+
# and is subject to an un-announced breaking change.
|
|
44
|
+
#
|
|
45
|
+
# Stockpile provides 5 methods as part of it's public API:
|
|
46
|
+
# * configuration
|
|
47
|
+
# * configure
|
|
48
|
+
# * perform_cached
|
|
49
|
+
# * redis
|
|
50
|
+
# * redis_connection_pool
|
|
51
|
+
module Stockpile
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
# Provides access to cache's configuration.
|
|
55
|
+
#
|
|
56
|
+
# @return [Configuration] the object holding configuration values
|
|
57
|
+
def configuration
|
|
58
|
+
@configuration ||= Configuration.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# API to configure cache dynamically during runtime.
|
|
62
|
+
#
|
|
63
|
+
# @yield [configuration] Takes in a block of code of code that is setting
|
|
64
|
+
# or changing configuration values
|
|
65
|
+
#
|
|
66
|
+
# @example Configure during runtime changing redis URL
|
|
67
|
+
# Stockpile.configure { |c| c.redis_url = 'foobar' }
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
def configure
|
|
71
|
+
yield(configuration)
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Attempts to fetch a value from cache (for a given key). In case of miss
|
|
76
|
+
# will execute given block of code and cache it's result at the provided
|
|
77
|
+
# key for a specified TTL.
|
|
78
|
+
#
|
|
79
|
+
# @param key [String] Key to use for a value lookup from cache or key
|
|
80
|
+
# to store value at once it is computed
|
|
81
|
+
# @param ttl [Integer] (optional) Time in seconds to expire cache after.
|
|
82
|
+
# Defaults to Stockpile::DEFAULT_TTL
|
|
83
|
+
#
|
|
84
|
+
# @yield [block] A block of code to be executed in case of cache miss
|
|
85
|
+
#
|
|
86
|
+
# @example Perform cache operation
|
|
87
|
+
# Stockpile.perform_cached(key: 'meaning_of_life', ttl: 42) { 21 * 2 }
|
|
88
|
+
#
|
|
89
|
+
# @return Returns a result of block execution
|
|
90
|
+
def perform_cached(key:, ttl: Stockpile::DEFAULT_TTL, &block)
|
|
91
|
+
Stockpile::CachedValueReader.read_or_yield(key: key, ttl: ttl, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# API to communicate with Redis database backing cache up.
|
|
95
|
+
#
|
|
96
|
+
# @yield [redis]
|
|
97
|
+
#
|
|
98
|
+
# @example Store a value in Redis at given key
|
|
99
|
+
# Store.redis { |r| r.set('meaning_of_life', 42) }
|
|
100
|
+
#
|
|
101
|
+
# @return Returns a result of interaction with Redis
|
|
102
|
+
def redis
|
|
103
|
+
redis_connection_pool.with do |connection|
|
|
104
|
+
yield connection
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Accessor to connection pool. Defined on top level so it can be memoized
|
|
109
|
+
# on the topmost level
|
|
110
|
+
#
|
|
111
|
+
# @return [ConnectionPool] ConnectionPool object from connection_pool gem
|
|
112
|
+
def redis_connection_pool
|
|
113
|
+
@redis_connection_pool ||= Stockpile::RedisConnection.connection_pool
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::Cache
|
|
19
|
+
#
|
|
20
|
+
# Wrapper around Stockpile.redis used for writing and reading from it; handles
|
|
21
|
+
# serialization and deserialization of data upon writes and reads.
|
|
22
|
+
module Cache
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def get(key:)
|
|
26
|
+
value_from_cache = Stockpile.redis { |r| r.get(key) }
|
|
27
|
+
Oj.load(value_from_cache) if value_from_cache
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_deferred(key:)
|
|
31
|
+
sleep(Stockpile::SLUMBER_COOLDOWN) until Stockpile.redis { |r| r.exists(key) }
|
|
32
|
+
value_from_cache = Stockpile.redis { |r| r.get(key) }
|
|
33
|
+
Oj.load(value_from_cache)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set(key:, payload:, ttl:)
|
|
37
|
+
payload = Oj.dump(payload)
|
|
38
|
+
Stockpile.redis { |r| r.set(key, payload) }
|
|
39
|
+
Stockpile.redis { |r| r.expire(key, ttl) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::CachedValueReader
|
|
19
|
+
#
|
|
20
|
+
# Service class to wrap decision point of wether a value should be
|
|
21
|
+
# returned from cache or computed and stored in cache
|
|
22
|
+
module CachedValueReader
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def read_or_yield(key:, ttl:, &block)
|
|
26
|
+
if (result = Stockpile::Cache.get(key: key))
|
|
27
|
+
result
|
|
28
|
+
else
|
|
29
|
+
Stockpile::Executor.perform(key: key, ttl: ttl, &block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::Configuration
|
|
19
|
+
#
|
|
20
|
+
# Holds configuration for cache with writeable attributes allowing
|
|
21
|
+
# dynamic change of configuration during runtime
|
|
22
|
+
class Configuration
|
|
23
|
+
attr_accessor :connection_pool, :connection_timeout, :lock_expiration,
|
|
24
|
+
:redis_url, :sentinels, :slumber
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@connection_pool = extract_connection_pool
|
|
28
|
+
@connection_timeout = extract_connection_timeout
|
|
29
|
+
@lock_expiration = extract_lock_expiration
|
|
30
|
+
@redis_url = extract_redis_url
|
|
31
|
+
@sentinels = process_sentinels
|
|
32
|
+
@slumber = extract_slumber
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def extract_connection_pool
|
|
38
|
+
ENV.fetch(
|
|
39
|
+
'STOCKPILE_CONNECTION_POOL',
|
|
40
|
+
Stockpile::DEFAULT_CONNECTION_POOL
|
|
41
|
+
).to_i
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_connection_timeout
|
|
45
|
+
ENV.fetch(
|
|
46
|
+
'STOCKPILE_CONNECTION_TIMEOUT',
|
|
47
|
+
Stockpile::DEFAULT_CONNECTION_TIMEOUT
|
|
48
|
+
).to_i
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def extract_lock_expiration
|
|
52
|
+
ENV.fetch(
|
|
53
|
+
'STOCKPILE_LOCK_EXPIRATION',
|
|
54
|
+
Stockpile::DEFAULT_LOCK_EXPIRATION
|
|
55
|
+
).to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_redis_url
|
|
59
|
+
ENV.fetch(
|
|
60
|
+
'STOCKPILE_REDIS_URL',
|
|
61
|
+
Stockpile::DEFAULT_REDIS_URL
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_slumber
|
|
66
|
+
ENV.fetch(
|
|
67
|
+
'STOCKPILE_SLUMBER',
|
|
68
|
+
Stockpile::DEFAULT_SLUMBER
|
|
69
|
+
).to_i
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process_sentinels
|
|
73
|
+
ENV.fetch('STOCKPILE_REDIS_SENTINELS', '').split(',').map do |sentinel|
|
|
74
|
+
host, port = sentinel.split(':')
|
|
75
|
+
{ host: host, port: port.to_i }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
DEFAULT_CONNECTION_POOL = 100
|
|
19
|
+
DEFAULT_CONNECTION_TIMEOUT = 3
|
|
20
|
+
DEFAULT_LOCK_EXPIRATION = 10
|
|
21
|
+
DEFAULT_REDIS_URL = 'redis://localhost:6379/1'
|
|
22
|
+
DEFAULT_SLUMBER = 2
|
|
23
|
+
DEFAULT_TTL = 60 * 5
|
|
24
|
+
LOCK_PREFIX = 'stockpile_lock::'
|
|
25
|
+
SLUMBER_COOLDOWN = 0.05
|
|
26
|
+
VERSION = '1.0.0'
|
|
27
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::Executor
|
|
19
|
+
#
|
|
20
|
+
# Executes passed in block of code and writes computed result into cache
|
|
21
|
+
# with an expiration of a given TTL. If execution is locked will wait for
|
|
22
|
+
# value to appear in cache instead. Will timeout after given amount of time
|
|
23
|
+
# and will execute block if no value can be read from cache.
|
|
24
|
+
class Executor
|
|
25
|
+
attr_reader :key, :ttl
|
|
26
|
+
|
|
27
|
+
def self.perform(key:, ttl:, &block)
|
|
28
|
+
new(key, ttl).perform(&block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(key, ttl)
|
|
32
|
+
@key = key
|
|
33
|
+
@ttl = ttl
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform(&block)
|
|
37
|
+
if execution(&block).success?
|
|
38
|
+
cache_and_release_execution
|
|
39
|
+
else
|
|
40
|
+
wait_for_cache_or_yield(&block)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def execution
|
|
47
|
+
@execution ||= Stockpile::Lock.perform_locked(lock_key: lock_key) do
|
|
48
|
+
yield
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cache_and_release_execution
|
|
53
|
+
Stockpile::Cache.set(
|
|
54
|
+
key: key,
|
|
55
|
+
payload: execution.result,
|
|
56
|
+
ttl: ttl
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
execution.release_lock
|
|
60
|
+
execution.result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def lock_key
|
|
64
|
+
Stockpile::LOCK_PREFIX + key
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def wait_for_cache_or_yield
|
|
68
|
+
Timeout.timeout(Stockpile.configuration.slumber) do
|
|
69
|
+
Stockpile::Cache.get_deferred(key: key)
|
|
70
|
+
end
|
|
71
|
+
rescue Timeout::Error
|
|
72
|
+
yield
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::FailedLockExecution
|
|
19
|
+
#
|
|
20
|
+
# Dummy class to symbolize failed locked execution of code.
|
|
21
|
+
class FailedLockExecution; end
|
|
22
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::Lock
|
|
19
|
+
#
|
|
20
|
+
# Attempts to set up exclusive lock to execute a block of code. Returns
|
|
21
|
+
# Stockpile::LockedExcutionResult holding result of execution. If lock
|
|
22
|
+
# can not be established (someone else is executing the code) then
|
|
23
|
+
# Stockpile::LockedExcutionResult will hold Stockpile::FailedLockExecution
|
|
24
|
+
# as a result of execution
|
|
25
|
+
class Lock
|
|
26
|
+
attr_reader :lock_key
|
|
27
|
+
|
|
28
|
+
def self.perform_locked(lock_key:, &block)
|
|
29
|
+
new(lock_key).perform_locked(&block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(lock_key)
|
|
33
|
+
@lock_key = lock_key
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform_locked(&block)
|
|
37
|
+
if lock
|
|
38
|
+
successful_execution(&block)
|
|
39
|
+
else
|
|
40
|
+
failed_execution
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def failed_execution
|
|
47
|
+
Stockpile::LockedExcutionResult.new(result: failed_lock, lock_key: lock_key)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def failed_lock
|
|
51
|
+
Stockpile::FailedLockExecution.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def lock
|
|
55
|
+
Stockpile.redis { |r| r.set(lock_key, 1, nx: true, ex: Stockpile.configuration.lock_expiration) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def successful_execution
|
|
59
|
+
Stockpile::LockedExcutionResult.new(result: yield, lock_key: lock_key)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::LockedExcutionResult
|
|
19
|
+
#
|
|
20
|
+
# Wrapper containing result of locked execution
|
|
21
|
+
class LockedExcutionResult
|
|
22
|
+
attr_reader :lock_key, :result
|
|
23
|
+
|
|
24
|
+
def initialize(lock_key:, result:)
|
|
25
|
+
@lock_key = lock_key
|
|
26
|
+
@result = result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def release_lock
|
|
30
|
+
Stockpile.redis { |r| r.expire(lock_key, 0) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success?
|
|
34
|
+
!result.is_a?(Stockpile::FailedLockExecution)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Stockpile
|
|
18
|
+
# == Stockpile::RedisConnection
|
|
19
|
+
#
|
|
20
|
+
# Wrapper around ConnectionPool and Redis to provide connectivity
|
|
21
|
+
# to Redis with desired configuration and sane connection pool
|
|
22
|
+
module RedisConnection
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def connection_pool
|
|
26
|
+
@connection_pool = ConnectionPool.new(connection_pool_options) do
|
|
27
|
+
Redis.new(connection_options)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def connection_options
|
|
32
|
+
{ url: redis_url,
|
|
33
|
+
sentinels: sentinels }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def connection_pool_options
|
|
37
|
+
{ size: pool_size,
|
|
38
|
+
timeout: connection_timeout }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def connection_timeout
|
|
42
|
+
Stockpile.configuration.connection_timeout
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pool_size
|
|
46
|
+
Stockpile.configuration.connection_pool
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def redis_url
|
|
50
|
+
Stockpile.configuration.redis_url
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sentinels
|
|
54
|
+
Stockpile.configuration.sentinels
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
|
|
6
|
+
require 'stockpile/constants'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |spec|
|
|
9
|
+
spec.name = 'stockpile_cache'
|
|
10
|
+
spec.version = Stockpile::VERSION
|
|
11
|
+
spec.authors = ['ConvertKit, LLC']
|
|
12
|
+
spec.email = ['engineering@convertkit.com']
|
|
13
|
+
|
|
14
|
+
spec.summary = 'Simple Redis based cache'
|
|
15
|
+
spec.description = 'Cache with cache-stampede protection'
|
|
16
|
+
spec.homepage = 'https://convertkit.com'
|
|
17
|
+
spec.license = 'Apache License Version 2.0'
|
|
18
|
+
|
|
19
|
+
spec.files = `git ls-files | grep -Ev '^(spec)'`.split("\n")
|
|
20
|
+
|
|
21
|
+
spec.executables = ['console']
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
spec.add_dependency 'connection_pool'
|
|
25
|
+
spec.add_dependency 'oj'
|
|
26
|
+
spec.add_dependency 'rake'
|
|
27
|
+
spec.add_dependency 'redis'
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stockpile_cache
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ConvertKit, LLC
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-09-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: connection_pool
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: oj
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: redis
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
description: Cache with cache-stampede protection
|
|
70
|
+
email:
|
|
71
|
+
- engineering@convertkit.com
|
|
72
|
+
executables:
|
|
73
|
+
- console
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- ".circleci/config.yml"
|
|
78
|
+
- ".gitignore"
|
|
79
|
+
- ".rspec"
|
|
80
|
+
- ".rubocop.yml"
|
|
81
|
+
- CODE_OF_CONDUCT.md
|
|
82
|
+
- Gemfile
|
|
83
|
+
- Gemfile.lock
|
|
84
|
+
- LICENSE.txt
|
|
85
|
+
- README.md
|
|
86
|
+
- Rakefile
|
|
87
|
+
- bin/console
|
|
88
|
+
- bin/setup
|
|
89
|
+
- lib/stockpile.rb
|
|
90
|
+
- lib/stockpile/cache.rb
|
|
91
|
+
- lib/stockpile/cached_value_reader.rb
|
|
92
|
+
- lib/stockpile/configuration.rb
|
|
93
|
+
- lib/stockpile/constants.rb
|
|
94
|
+
- lib/stockpile/executor.rb
|
|
95
|
+
- lib/stockpile/failed_lock_execution.rb
|
|
96
|
+
- lib/stockpile/lock.rb
|
|
97
|
+
- lib/stockpile/locked_execution_result.rb
|
|
98
|
+
- lib/stockpile/redis_connection.rb
|
|
99
|
+
- lib/stockpile_cache.rb
|
|
100
|
+
- stockpile-cache.gemspec
|
|
101
|
+
homepage: https://convertkit.com
|
|
102
|
+
licenses:
|
|
103
|
+
- Apache License Version 2.0
|
|
104
|
+
metadata: {}
|
|
105
|
+
post_install_message:
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 3.0.3
|
|
121
|
+
signing_key:
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: Simple Redis based cache
|
|
124
|
+
test_files: []
|