nitro_pg_cache 0.1.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +475 -0
- data/Rakefile +37 -0
- data/config/initializers/inflections.rb +3 -0
- data/db/migrate/1_add_nitro_cache_tables.rb +33 -0
- data/lib/models/nitro_cache.rb +24 -0
- data/lib/models/nitro_partial.rb +184 -0
- data/lib/nitro_pg_cache.rb +24 -0
- data/lib/nitro_pg_cache/acts_as_nitro_cacheable.rb +8 -0
- data/lib/nitro_pg_cache/engine.rb +5 -0
- data/lib/nitro_pg_cache/model_ext.rb +90 -0
- data/lib/nitro_pg_cache/version.rb +3 -0
- data/lib/nitro_pg_cache/viewer_ext.rb +164 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8e8a7e2dcc650c34494a5a2b8a5f701f7ec95023
|
4
|
+
data.tar.gz: 9bd438f3d4f30f738b4d750061585b1e5e821f3c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6a4ab1152c2b6f30c8db51491cddd697f223a1c7d443949da17ac0b36e9c30f084a218f2595cc1e0190983a6efa045c29882fe6f4a6a76d01ced16116de20208
|
7
|
+
data.tar.gz: 486d149799dcd32d72d13c43be979848208f232d5843897c78bef8cf6c069b2720814f885ef6d02b0ebb74ce69ed4110436a063de0d22a4a03ff4385cc71c534
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 alekseyl
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,475 @@
|
|
1
|
+
# ATTENTION
|
2
|
+
To use this gem you need PostgreSQL 9.4 and higher!
|
3
|
+
|
4
|
+
# ATTENTION(РУС)
|
5
|
+
Для корректной работы библиотеки требуется версия PostgreSQL не ниже 9.4!
|
6
|
+
|
7
|
+
# NitroPgCache
|
8
|
+
This gem create DB-based solution for caching relation collections. It based on PostgreSQL version >= 9.4 .
|
9
|
+
It faster than memcache+dalli combination. In some cases three times faster, but with all DB facilities!
|
10
|
+
|
11
|
+
Right now nitro_pg_cache is in alpha-release state. It's working, but may need additional tuning and features, for example limits and expiring,
|
12
|
+
actually I don't know which will suit best.
|
13
|
+
|
14
|
+
# FEATURES
|
15
|
+
Already working*:
|
16
|
+
(* all benchmark numbers are given with pg_cache_key gem enabled, this mean that in rails < 5 or without pg_cache_key, you'll get +25% additional speed bonus for cached collection )
|
17
|
+
|
18
|
+
1. First rendering is faster then memcache+dalli on same machine. ~10% faster
|
19
|
+
2. Reordering and sub-collection rendering on cached collection are 3 times faster then memcached+dalli**
|
20
|
+
Rendering are done with DB speed i.e. You can assume that feed rendering speed now are some very small time constant. 100 and 1K records are rendering with
|
21
|
+
~0.01s difference
|
22
|
+
( partially rendered subcollection is a superposition of 1.15 and 3 times, depends of non-cached elems amount )
|
23
|
+
** it's assymptotic value, when collection rendering takes much more time than other rendering parts,
|
24
|
+
when collection is small and collection rendering is comparable to other page parts rendering you'll get less than 3
|
25
|
+
|
26
|
+
3. You can enable prerendering for any scope of your DB records ( if you has a reasonable amount of different keys|locals sets per db record )
|
27
|
+
4. Agile managing of your cache because it's in DB now, you for example don't need to touch elems to remove
|
28
|
+
their cache you can do: NitroCache.where(nitro_cacheable: collection).delete_all
|
29
|
+
5. In 4.x rails if you don't use pg_cache_key, but use nitro_cache then you get additional +25% speedup for completly cached feed
|
30
|
+
|
31
|
+
Can be done soon
|
32
|
+
|
33
|
+
6. Easily can switch back and forth from usual cache to db cache using cache_by key.
|
34
|
+
7. Shards DB
|
35
|
+
8. auto-renewable cache. we can save locals to jsonb column, after touching cached-element we can rerender
|
36
|
+
all dependent caches with saved locals. Differs from prerender that we don't prerender all possibilities, but only already rendered
|
37
|
+
( must check how mass update will suffer from json insert ).
|
38
|
+
9. Expiring and quantity limits, cache expiring can be done on different conditions including last time viewed.
|
39
|
+
|
40
|
+
# NitroPgCache (РУС)
|
41
|
+
Данная библиотека реализует кеширование relation-коллекций на основе движка PostgreSQL последних версий (>=9.4).
|
42
|
+
Получившийся результат по всем показателям скороси не уступает
|
43
|
+
классической схеме memcache+dalli, а во многих случаях и превосходит ее в разы, обладая при этом всеми достоинствами базы данных.
|
44
|
+
|
45
|
+
В настоящий момент библиотека находится в состоянии alpha-release. Основной функционал ее работает, но ряд дополнительных возможностей требует реализации.
|
46
|
+
Например ограничения на количество кешей, устаревание кешей и пр. . Если есть какие-то пожелания какие конкретно должны быть возможности связанные с этим: велкам
|
47
|
+
|
48
|
+
# ВОЗМОЖНОСТИ БИБЛИОТЕКИ
|
49
|
+
|
50
|
+
Реализованные возможности*:
|
51
|
+
(* величины указаны при использовании гема pg_cache_key для реализации cache_key у коллекций,
|
52
|
+
поэтому в 4-х рельсах nitro_cache получает еще +25% выигрыша по времени, если pg_cache_key не исопльзуется, даже на полностью кешированной коллекции, см 6) )
|
53
|
+
|
54
|
+
1. Первичный рендеринг быстрее чем у memcache+dalli на ~10% для коллекции ( Это малоактуально если рендеринг коллекции занимает
|
55
|
+
менее 50% от рендеринга всей страницы, т.е. выигрыш на всей странице становится ~ 5% )
|
56
|
+
2. Пересортировка или рендеринг подколлекций на закешированной матрешкой коллекции в 2-3 и более раз быстрее ( чем больше коллекция тем больше выигрыш )
|
57
|
+
3. Возможность пререндеринга для элементов, т.е. при обновлении кешируемого элемента его кеши обновляются автоматом + спец рейк на их первичную генерацию
|
58
|
+
3.a Возможность пререндеринга только для определенного scope элементов.
|
59
|
+
4. Управление кешем на уровне БД. Например сброс кешей можно делать без того чтобы трогать объекты: NitroCache.where(nitro_cacheable: collection).delete_all и пр.
|
60
|
+
5. В четвертых рельсах, если не использовать gem pg_cache_key дополнительно выигрывает 25% времени от memcached+dalli
|
61
|
+
|
62
|
+
Нереализованные пока
|
63
|
+
|
64
|
+
6. Можно легко переключать между обычным кешем и бд кешем. используя cache_by ?
|
65
|
+
7. БД Шардинг
|
66
|
+
8. Авторекеш. Возможно в дальнейшем автоматичечки перекешировывать существующие кеши без использования prerender - true, а с сохранением к каждому
|
67
|
+
ключу еще и Json для локалс.
|
68
|
+
9. Устаревание И лимитирование кешей. Может быть реализовано многими способами.
|
69
|
+
|
70
|
+
## RESTRICTIONS:
|
71
|
+
|
72
|
+
Only clear collections rendering can be cached with this gem. i.e.:
|
73
|
+
Can convert:
|
74
|
+
```
|
75
|
+
-cache [@records, locals ] do
|
76
|
+
=render partial: 'record', collection: @records, locals: locals
|
77
|
+
```
|
78
|
+
Can't convert ( you will need to split it )
|
79
|
+
```
|
80
|
+
-cache [@records, locals ] do
|
81
|
+
=render partial: 'record', collection: @records, locals: locals
|
82
|
+
=render partial: 'pagination_footer', records: @records
|
83
|
+
```
|
84
|
+
|
85
|
+
## ОГРАНИЧЕНИЯ:
|
86
|
+
|
87
|
+
Только чистый кеш на коллекцию может использоваться с данной библиотекой:
|
88
|
+
Может быть сконвертированно:
|
89
|
+
```
|
90
|
+
-cache [@records, locals] do
|
91
|
+
=render partial: 'record', collection: @records, locals: locals
|
92
|
+
```
|
93
|
+
|
94
|
+
Не получится сконвертировать без изменений ( придется разделить коллекцию и футер)
|
95
|
+
|
96
|
+
```
|
97
|
+
-cache [@records, locals ] do
|
98
|
+
=render partial: 'record', collection: @records, locals: locals
|
99
|
+
=render partial: 'pagination_footer', records: @records
|
100
|
+
```
|
101
|
+
|
102
|
+
## CACHING ALGORITHMS ( STRAIGHT/REVERSE/ARRAY CACHING )
|
103
|
+
|
104
|
+
Three types of caching collection mechanism are used: straight, 'reverse', array-elem
|
105
|
+
straight and reverse used for relation objects! array-elem - instantinated array or elem
|
106
|
+
|
107
|
+
straight (db_cache_collection_s) - similar to usual cache, we check does every elements already cached, if so we just return aggregation result,
|
108
|
+
if not - we just add +1 join on nitro_caches +1 select for nitro_cached_value as virtual attribute,
|
109
|
+
then we render element if nitro_cached_value.nil? or use nitro_cached_value otherwise.
|
110
|
+
|
111
|
+
'reverse' (db_cache_collection_r) - is not similar to usual cache algorithms it used 'reversed' logic: we create special SQL-query only for non-cached
|
112
|
+
elements, render them, and then we use aggregation on a previously given collection. This special SQL-query use all includes, joins, select which was in original
|
113
|
+
query so we successfully escaping N+1 problems same way as usual cache did.
|
114
|
+
This approach gives us more speed even on whole noncached collection. How it possible? Less string concatenation, less reallocation e.t.c
|
115
|
+
|
116
|
+
array-elem (db_cache_array) - this is method used only with prerender: true for changed record.
|
117
|
+
DON'T USE IT ELSEWHERE!! If you have complex hierarchy of models and don't include them on update action of your controller
|
118
|
+
it may give you N+1 problem internally.
|
119
|
+
|
120
|
+
## ВАРИАНТЫ КЕШИРОВАНИЯ ( STRAIGHT/REVERSE/ARRAY CACHING )
|
121
|
+
|
122
|
+
Прямой и реверсивный ( straight and reverse ) используются только для relation объектов. array cache используется только если prerender: true
|
123
|
+
для измененного элемента.
|
124
|
+
|
125
|
+
Прямое кеширование (db_cache_collection_s): мы классическим образом сначала проверяем что вся коллекция закеширована. Если закеширована - запросом с исолпьзованием str_agg собираем фид
|
126
|
+
коллекции на уровне БД. нет - идем последовательно и рендерим, если нет кеша, или подставляем кеш, если он есть.
|
127
|
+
|
128
|
+
Реверс кеширование (db_cache_collection_r): Мы делаем дополнительный специальный SQL-запрос, с учетом тех настроек инклюдов джойнов и селектов которые есть в
|
129
|
+
полученном relation, но с условием что выбираются только элементы по которым нет кеша. прогоняем по ним рендеринг коллекции.
|
130
|
+
и после этого проводим запрос на аггрегацию фида. Данный вариант кеширования оказался быстрее как прямого кеширования, так и обычной связки memcache + dali
|
131
|
+
( я думаю из-за того что меньше работы со строками ).
|
132
|
+
|
133
|
+
array-elem (db_cache_array) - Этот метод используется только для prerender=true, только для тех элементов которые изменились.
|
134
|
+
НЕ ИСПОЛЬЗУЙТЕ ЕГО НИГДЕ В ДРУГИХ СЛУЧАЯХ!
|
135
|
+
|
136
|
+
|
137
|
+
## BENCHMARK VS MEMCACHE + DALLI
|
138
|
+
|
139
|
+
Comparisons were made manually with rack mini-profiler gem +
|
140
|
+
I used htop system-monitor to be sure that nothing going in the background and tempering with results
|
141
|
+
|
142
|
+
CONFIGURATION: dalli + memcached same machine vs postgres 9.4 same machine
|
143
|
+
VM config: 8 logical cores, Core i7 SSD 10Gb RAM
|
144
|
+
|
145
|
+
|
146
|
+
ATTENTION NOTICE:
|
147
|
+
This numbers just a VERY particular case, you can use them to predict your own *comparative* numbers very carefully,
|
148
|
+
and of course you can't predict your own time in seconds!
|
149
|
+
But I did it on two completly different tables and their collections and get very closed
|
150
|
+
result in percents meaning that numbers are quite representative
|
151
|
+
|
152
|
+
first column - records count
|
153
|
+
rv - "reverse" cache when we first just render only __missing__ elements and save them to DB, and second aggregate all collection from db
|
154
|
+
dbs - data base straight mean it's render's with one straightforward iteration
|
155
|
+
mmch - usual matroska doll cache with memcached and dalli
|
156
|
+
|
157
|
+
### FIRST RENDER
|
158
|
+
|
159
|
+
| Records count | 'Reverse' nitro cache | Straight nitro cache | Memcache | Ratio |
|
160
|
+
|---------------|-----------------------|----------------------|----------|--------|
|
161
|
+
|1K | 14.7s | 16s | 17s | ~1.15 faster |
|
162
|
+
|0.38K | 4.9s | 5.2s | 5.6s | |
|
163
|
+
|0.1K | 1.3s | 1.4s | 1.5s | ~1.15 faster |
|
164
|
+
|
165
|
+
### FIRST RENDER SUBCOLLECTION/REORDERING (i.e. when all elements are cached, but not whole collection)
|
166
|
+
|
167
|
+
| Records count | 'Reverse' nitro cache | Memcache | Ratio |
|
168
|
+
|---------------|-----------------------|----------|--------|
|
169
|
+
|1K | 0.5s | 1.5s | ~3 times faster |
|
170
|
+
|0.38K | 0.35+s | 0.75+ | ~2 times faster |
|
171
|
+
|0.12K | 0.2-0.25 | 0.4-0.5+s | ~2 times faster |
|
172
|
+
|
173
|
+
### PARTIAL COLLECTION RENDERING
|
174
|
+
We can assume this is superposition of already obtained numbers. i.e inside range: 1.15-3
|
175
|
+
( the right borders number depends on the collection size, the bigger collection bigger the number )
|
176
|
+
|
177
|
+
### GETTING COLLECTION CACHE ( whole collection cached ( nitro wins cause it's not need to instantinate collection ) )
|
178
|
+
|
179
|
+
!Notice: Next comparision is valid only for rails <=4.2 without pg_cache_key gem!
|
180
|
+
In rails >= 5 or with pg_cache_key gem it will bring nearly same result i.e. ratio will be 1!
|
181
|
+
|
182
|
+
| Records count | 'Reverse' nitro cache | Memcache | Ratio |
|
183
|
+
|---------------|-----------------------|----------|--------|
|
184
|
+
|1K | 0.5s | 1s | ~2 times faster |
|
185
|
+
|0.38K | 0.35+s | 0.6 | ~1.7 times faster |
|
186
|
+
|0.12K | 0.2-0.25 | 0.38s | ~1.7 times faster |
|
187
|
+
|
188
|
+
|
189
|
+
## СРАВНЕНИЕ С MEMCACHE + DALLI
|
190
|
+
|
191
|
+
Сравнения провел вручную на живых страницах с исопльзованием rack mini-profiler gem.
|
192
|
+
Используя htop, следил, чтобы ничего не загружало систему дополнительно и портило результаты.
|
193
|
+
|
194
|
+
Настройки виртуалки: dalli + memcached vs postgres 9.4 ( ~ 8 логических ядер + 10Gb, реальная машина Core i7 SSD 16Gb RAM)
|
195
|
+
|
196
|
+
ВНИМАНИЕ:
|
197
|
+
|
198
|
+
Полученные результаты это конкретный случай, их нельзя использовать для того чтобы предстказать ваш результат в секундах,
|
199
|
+
НО с определенной осторожностью можно предсказать относительные значения. Я прогонял тест на двух совершенно разных
|
200
|
+
таблицах/коллекциях и сравнительные значения были примерно одинаковые.
|
201
|
+
|
202
|
+
Расшифровка таблицы:
|
203
|
+
first column - Количество записей из таблицы
|
204
|
+
rv - (reverse) время потраченное при реверсивном кешировании/рендеринге
|
205
|
+
dbs - (data base straight) кеширование/рендеринг осуществляется в один прямой проход
|
206
|
+
mmch - (memcache) обычный матрешный кеш memcached + dalli
|
207
|
+
|
208
|
+
### Первичный рендеринг
|
209
|
+
|
210
|
+
| Records count | 'Reverse' nitro cache | Straight nitro cache | Memcache | Ratio |
|
211
|
+
|---------------|-----------------------|----------------------|----------|--------|
|
212
|
+
|1K | 14.7s | 16s | 17s | ~1.15 faster |
|
213
|
+
|0.38K | 4.9s | 5.2s | 5.6s | |
|
214
|
+
|0.1K | 1.3s | 1.4s | 1.5s | ~1.15 faster |
|
215
|
+
|
216
|
+
### Первичный рендеринг на подколлекцию или со сменой порядка (т.е. все отдельные элементы уже закешированы, но не вся коллекция)
|
217
|
+
|
218
|
+
| Records count | 'Reverse' nitro cache | Memcache | Ratio |
|
219
|
+
|---------------|-----------------------|----------|--------|
|
220
|
+
|1K | 0.5s | 1.5s | ~3 times faster |
|
221
|
+
|0.38K | 0.35+s | 0.75+ | ~2 times faster |
|
222
|
+
|0.12K | 0.2-0.25 | 0.4-0.5+s | ~2 times faster |
|
223
|
+
|
224
|
+
### Частично закешированая коллекция
|
225
|
+
Можно точно предположить что время/быстродействие будет суперпозицией от первых двух результатов и будет лежать в пределах 1.15-3
|
226
|
+
|
227
|
+
### Получение полного кеша коллекции ( только для рельс < 5 )
|
228
|
+
|
229
|
+
Обращаю внимание что в связи с отличием в получение cache_key на коллекции между rails 4.2 и rails 5
|
230
|
+
Данные цифры актуальны только для старых версий рельс без использования моего гема pg_cache_key
|
231
|
+
В противному случае результаты будут примерно одинаковые!!
|
232
|
+
|
233
|
+
| Records count | 'Reverse' nitro cache | Memcache | Ratio |
|
234
|
+
|---------------|-----------------------|----------|--------|
|
235
|
+
|1K | 0.5s | 1s | ~2 times faster |
|
236
|
+
|0.38K | 0.35+s | 0.6 | ~1.7 times faster |
|
237
|
+
|0.12K | 0.2-0.25 | 0.38s | ~1.7 times faster |
|
238
|
+
|
239
|
+
## MEMORY USAGE
|
240
|
+
|
241
|
+
I didn't make a special comparision, but I assume that there is a insufficient difference between usual cache and pg_cache.
|
242
|
+
|
243
|
+
|
244
|
+
## ИСПОЛЬЗОВАНИЕ ПАМЯТИ
|
245
|
+
|
246
|
+
Объем используемой памяти примерно одинаковый и может зависеть от того какой конкретно пришел запрос, сколько в нем уже
|
247
|
+
закешированных элементов сколько новых и пр.
|
248
|
+
|
249
|
+
|
250
|
+
## GENERAL FALLBACK
|
251
|
+
With any variant of prerender true/false all not found caches get themselves cached usual way as in prerender-false case. i.e. as usual cache will do.
|
252
|
+
|
253
|
+
## ОСНОВНОЕ ПОВЕДЕНИЕ ПО УМОЛЧАНИЮ
|
254
|
+
Независимо от того стоит prerender-true или нет, если на момент запроса значение nitro_cache_value пустое,
|
255
|
+
то кеширование запускается обычным ходом, который совпадает, с вариантом когда prerender-false.
|
256
|
+
|
257
|
+
|
258
|
+
## HOW IT BEHAVE WHEN SOMETHING CHANGES ( KEYS, PARTIALS, ETC )
|
259
|
+
The main rule of thumb: no prerendering at server start, only mass cleaning old and creating new nitro_partial records!
|
260
|
+
If you are using prerender, then run rake task prerender in parallel manually or by any automation script
|
261
|
+
The rules of cache changes are depended on prerender state of partial true|false
|
262
|
+
|
263
|
+
*Object changes:*
|
264
|
+
|
265
|
+
1. prerender-true => after_commit -> render all locals variants
|
266
|
+
2. prerender-false => after_commit -> clear all caches
|
267
|
+
|
268
|
+
*Cache params changes:*
|
269
|
+
|
270
|
+
1. New keys added.
|
271
|
+
prerender true => rails started as usual, you run prerender rake manually!!,
|
272
|
+
prerender false => do nothing! Everything will be rendered on demand!
|
273
|
+
|
274
|
+
2. Keys were removed => nitro_partial.db_cached_partials.where.not( nitro_partial.cache_keys.keys ).delete_all at rails start.
|
275
|
+
|
276
|
+
3. New partial
|
277
|
+
+ the new nitro_partial record would be added to DB at rails start if we use prerender or at first render otherwise
|
278
|
+
all prerendering only in rake!
|
279
|
+
4. Partial changed.
|
280
|
+
+ remove all obsolete keys from DB at rails start
|
281
|
+
5. Removing partial. all obsolete cache keys will be deleted at application start
|
282
|
+
6. Prerender -> toggle
|
283
|
+
+ true -> false => do_nothing
|
284
|
+
+ false -> true => manually run rake :nitro_prerender to prerender those who don't exists.
|
285
|
+
7. partial naming changes.
|
286
|
+
+ it's possible to create rake rename_nitro_partial but right now you just rename your partial -
|
287
|
+
loose all rendered cache pieces and rerender them as if you create new one.
|
288
|
+
Also it's possible to change cache key mechanism generation and use not the file name, but file content
|
289
|
+
hash_key, then any renaming and moving of a file will not affect any cached values. Now it's not the point.
|
290
|
+
8. When expiration params changes При изменении параметров устаревания, проверяем в rake :expire_db_nitro_cache который можно в кронджобы вписать.
|
291
|
+
все кеши на соответствие новым правилам. Ненужное удаляем.
|
292
|
+
|
293
|
+
## ПРАВИЛА ИЗМНЕНИЯ КЕША, ЕСЛИ ЧТО_ТО ПОМЕНЯЛОСЬ (РУС)
|
294
|
+
Главное правило: никакого пререндеринга на старте сервера иначе у деплоя развяжется пупок.
|
295
|
+
На старте только: массовое удаление устаревшего, создание новых записей nitro_partial для новых паршиалов.
|
296
|
+
Правила поведения кеша при изменениях ( поведение зависит от значения prerender - true|false)
|
297
|
+
*При изменении объекта:*
|
298
|
+
|
299
|
+
1. prerender-true => after_commit -> render all variants
|
300
|
+
2. prerender-false => after_commit -> clear, view on demand -> render and mass save
|
301
|
+
|
302
|
+
*При изменении параметров кеша:*
|
303
|
+
|
304
|
+
1. Добавились новые ключи.
|
305
|
+
true => рельсы стартуют без дополнительного пререндеринга, параллельно запускаем rake :nitro_prerender,
|
306
|
+
false => do nothing! Ключей не было, значений не было, все будет генериться по первому требованию
|
307
|
+
2. Удалились ключи true/false => nitro_partial.db_cached_partials.where.not( nitro_partial.keys ).delete_all на старте приложения можно.
|
308
|
+
+ Если уже сгенеренные значения не важны то можно просто переписать код, кеши для не найденных файлов будут удалены,
|
309
|
+
новые можно прерндернуть соответствующим рейком
|
310
|
+
3. При изменении параметров устаревания, проверяем в rake :expire_db_nitro_cache который можно в кронджобы вписать.
|
311
|
+
все кеши на соответствие новым правилам. Ненужное удаляем.
|
312
|
+
|
313
|
+
## PARTIAL PRERENDER
|
314
|
+
|
315
|
+
Since nitro_pg_cache works as usual cache* also we can prerender only for part of keys and part of records, only most wanted.
|
316
|
+
|
317
|
+
For example I have feed different for admin and user, but since admin can wait more and also looks at the feed not
|
318
|
+
very often. So I can set for prerender locals: { role: [User] }, instead of locals: { role: [User, Admin] }
|
319
|
+
and get twice less prerendered caches.
|
320
|
+
|
321
|
+
Another example: we have a long history of payments but actual need is only for a last year for example,
|
322
|
+
so we can set prerender scope with condition on :created_at column, and prerender only a last year records.
|
323
|
+
|
324
|
+
*see section LIMITATIONS for more details on the possibilities of replacing usual feed cache with nitro
|
325
|
+
|
326
|
+
## ЧАСТИЧНЫЙ ПРЕРЕНДЕРИНГ
|
327
|
+
В силу того что nitro_cache может работать практически как обычный матрешный кеш* мы можем включить пререндеринг только для части
|
328
|
+
ключей и части записей.
|
329
|
+
|
330
|
+
Например у меня разное отображение ленты для админа и для пользователя. Админ пользуется лентой нечасто и в целом может
|
331
|
+
подождать на полсекунды дольше. ТО в параметрах пререндеринга можно написать locals: { role: [User] }, вместо locals: { role: [User, Admin] }
|
332
|
+
и пререндрить вполовину меньше вариантов для записи.
|
333
|
+
|
334
|
+
Второй пример: мы ведем длинную историю оплат пользователей, но для работы бухов нужен последний квартал или там год
|
335
|
+
мы можем выставить scope для пререндеринга по :created_at и пререрндерить только нужные записи.
|
336
|
+
|
337
|
+
## EXPIRING
|
338
|
+
Right now all cache get timestamp for the last access ( :viewed_at ) so it possible to control cache expiration on time basis
|
339
|
+
|
340
|
+
## УСТАРЕВАНИЕ
|
341
|
+
Сейчас все ключи хранят штамп времени последнего просмотра поэтому можно легко реализовать устаревающий кеш. например как рейк + крон-джоб
|
342
|
+
|
343
|
+
|
344
|
+
## STRAIGHT VS REVERSE VS CLASSIC POSSIBLE PROBLEMS
|
345
|
+
1. DB Sharding for reverse-cache. If we use db sharding reverse-cache may need additional tuning and testing since it's doing its job
|
346
|
+
in two steps. Straight-cache will work anyway.
|
347
|
+
2. Exotic cases for any variant of pg_cache. If we render same collection twice with different partial inside one controller action ( it's quite unusual behaviour ),
|
348
|
+
than we may instatinate collection twice.
|
349
|
+
|
350
|
+
## STRAIGHT VS REVERSE VS CLASSIC ВЕРОЯТНЫЕ ПРОБЛЕМЫ
|
351
|
+
1. БД-шардинг при обратном кешировании. Если мы используем БД шардинг, то вариант реверс кеширования требует доработки, потому что мы должны спрашивать
|
352
|
+
аггрегацию сразу же после того как сделали апдейт, поэтому по идее это должно идти на мастер-шард.
|
353
|
+
2. Экзотические варинты многоразового рендеринга с разными паршиалами одной коллекции в одном методе контроллера. Тогда может быть многоразовая инстантинация
|
354
|
+
на первом рендеринге.
|
355
|
+
|
356
|
+
|
357
|
+
NitroPartial STRUCTURE AND SPECS
|
358
|
+
1. В БД паршиалы уникальны по относительным путям
|
359
|
+
2. Параметры специфические для паршиала ( сколько хранить записей и когда экспайрить, можно задавать прямо в рендер )
|
360
|
+
2. При загрузке мы все паршиалы из БД всасываем в хеш partials_cache = { path: Partial }, проверяем наличие файлов,
|
361
|
+
если какого-то нет - вычищаем БД от его записей.
|
362
|
+
3. Проверяем что хеши на контент файлов не поменялись, для всех которые поменялись - удаляем кеш-записи
|
363
|
+
4. При запросе объекта Partial по путю из partials_cache в случае отсутствия оного - он сначала заправшивается в БД
|
364
|
+
( это случай когда он параллельно был создан в соседнем процессе ) и если такого нет создается новый.
|
365
|
+
|
366
|
+
|
367
|
+
## Usage
|
368
|
+
|
369
|
+
### As cache replacement
|
370
|
+
Replace:
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
-cache [@requests] do
|
374
|
+
=render partial: "admins/requests/request", collection: @requests, cached: true
|
375
|
+
```
|
376
|
+
|
377
|
+
with:
|
378
|
+
```ruby
|
379
|
+
=db_cache_collection( collection: @requests, partial: "admins/requests/request.html.slim", as: :request, locals: {} )
|
380
|
+
```
|
381
|
+
|
382
|
+
inside request.html.slim replace:
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
-cache[request] do
|
386
|
+
```
|
387
|
+
|
388
|
+
with:
|
389
|
+
```ruby
|
390
|
+
-db_cache(request) do
|
391
|
+
```
|
392
|
+
|
393
|
+
and inside your model:
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
class Request < ActiveRecord::Base
|
397
|
+
acts_as_nitro_cacheable
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
401
|
+
### Prerender
|
402
|
+
|
403
|
+
If you want to use prerender model will look like:
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
class Request < ActiveRecord::Base
|
407
|
+
acts_as_nitro_cacheable
|
408
|
+
add_prerender_partial( partial: "admins/requests/request.html.slim",
|
409
|
+
all_locals: {},
|
410
|
+
as: :request,
|
411
|
+
scope: Request.except_created.with_deleted.where(created_at: -6.month.from_now..Time.now ) )
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
|
416
|
+
## Installation
|
417
|
+
Add this line to your application's Gemfile:
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
gem 'nitro_pg_cache'
|
421
|
+
```
|
422
|
+
|
423
|
+
And then execute:
|
424
|
+
```bash
|
425
|
+
$ bundle install
|
426
|
+
```
|
427
|
+
|
428
|
+
Or install it yourself as:
|
429
|
+
```bash
|
430
|
+
$ gem install nitro_pg_cache
|
431
|
+
```
|
432
|
+
|
433
|
+
```bash
|
434
|
+
$ rake nitro_pg_cache:install:migrations
|
435
|
+
$ rake db:migrate
|
436
|
+
```
|
437
|
+
|
438
|
+
## Contributing
|
439
|
+
Issues and PR are welcome.
|
440
|
+
|
441
|
+
## TODO
|
442
|
+
0. Нужно config в рельсах приделать. по аналогии с конфигом для дали например.
|
443
|
+
|
444
|
+
1. выписать какие из вариантов для пререндеринга не прошли пользовательского тестирования, проверить работу с комбинациями ключей
|
445
|
+
2. Обновление прогресса для rake db_cache по количеству записей, а не по уникальным ключам шаги.
|
446
|
+
Вариант: вынести получение незакешированной части коллекции в отдельный метод, получить общее количество незакешированных элементов,
|
447
|
+
переопределить рядом в рейке db_cache с вызовом inc на прогресс внутри.
|
448
|
+
3. устаревание expires?
|
449
|
+
4. Общие лимиты. можно устанавливать через конфиг на всех и на отдельные паршиалы следить
|
450
|
+
за переполнением можно в кронджобах
|
451
|
+
5. rspec (!!) Для целей тестирования достаточно переопределить ф-ию аггрегации и можно тестировать на sqlite :memory, т.е. в принципе замокать ее в спеках,
|
452
|
+
остальное должно работать везде. + замокать cashe_keys возможно придется также ( если не сработает: https://sqlite.org/json1.html )
|
453
|
+
|
454
|
+
|
455
|
+
## Possibilities for the future
|
456
|
+
Возможные направления для дальнейшей оптимизации рендеринга коллекций:
|
457
|
+
|
458
|
+
1. паралельный рендеринг коллекции неоткешированных элементов, в силу того что нам не надо морочить голову с очерендностью рендеринга,
|
459
|
+
можно параллельно отрендерить N подколлекций и бабахнуть это на БД одним запросом, скорее всего GIL не должен
|
460
|
+
мешать потому что они не пересекаются и пр.
|
461
|
+
|
462
|
+
Актуально если мы не можем использовать prerender ( например у нас комбинаций ключей получается оч много ) НО почти неактуально, если мы используем пререндер.
|
463
|
+
|
464
|
+
Также возможно: автоматическое получение параметров с которых начинает иметь смысл параллелить
|
465
|
+
|
466
|
+
2. Еще одна возможность разгона рельсового приложения с получением коллекций: nginx-sql модуль. Имеет смыссл если коллекция будет получаться
|
467
|
+
отдельным запросом.
|
468
|
+
|
469
|
+
3. Настраиваемый размер коллекции при которой происходит переключение между дополнительным кешированием всей коллекции или же остаемся в рамках запроса на склейку строк.
|
470
|
+
Сейчас сделан выбор в пользу оптимизации больших коллекций т.е. делается еще дополнительные запросы в БД, что дает нам еще
|
471
|
+
большую скорость на уже закешированных больших коллекциях, но на маленьких будет наверняка дороже чем сразу склеить результат.
|
472
|
+
|
473
|
+
|
474
|
+
## License
|
475
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'NitroPgCache'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
require 'bundler/gem_tasks'
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task default: :test
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class AddNitroCacheTables < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :nitro_caches do |t|
|
4
|
+
t.text :key
|
5
|
+
t.text :nitro_cached_value
|
6
|
+
|
7
|
+
t.references :nitro_cacheable, polymorphic: true, index: { name: :index_nitro_cacheable_relations }
|
8
|
+
t.references :nitro_partial, index: true
|
9
|
+
|
10
|
+
# we don't need index here IF we never expire cache! also
|
11
|
+
# since we use cron job to remove expired indexes, when index starts to cost lot we may omit
|
12
|
+
t.datetime :viewed_at, index: true
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :nitro_caches, [:nitro_cacheable_id, :nitro_cacheable_type, :key], name: :merged_nitro_cacheable_index, unique: true
|
16
|
+
add_index :nitro_caches, :nitro_cache_key, where: 'nitro_cacheable_id = NULL AND nitro_cacheable_type = NULL'
|
17
|
+
|
18
|
+
create_table :nitro_partials do |t|
|
19
|
+
t.text :partial, index: true
|
20
|
+
t.boolean :prerender
|
21
|
+
t.string :expires
|
22
|
+
t.integer :record_limit, limit: 8
|
23
|
+
t.string :partial_hash, index: true
|
24
|
+
t.string :render_as
|
25
|
+
t.column :cache_keys, :jsonb, default: {}, null: false
|
26
|
+
|
27
|
+
t.timestamps null: false
|
28
|
+
end
|
29
|
+
|
30
|
+
add_index :nitro_partials, :cache_keys, using: :gin
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: nitro_caches
|
4
|
+
#
|
5
|
+
# id :integer not null, primary key
|
6
|
+
# nitro_cache_key :text
|
7
|
+
# nitro_cached_value :text
|
8
|
+
# nitro_cacheable_id :integer
|
9
|
+
# nitro_cacheable_type :string
|
10
|
+
# nitro_partial_id :integer
|
11
|
+
# viewed_at :datetime
|
12
|
+
#
|
13
|
+
# Indexes
|
14
|
+
#
|
15
|
+
# index_db_cache_partials_relations (nitro_cacheable_type,nitro_cacheable_id)
|
16
|
+
# index_nitro_caches_on_nitro_cache_key (nitro_cache_key)
|
17
|
+
# index_nitro_caches_on_viewed_at (viewed_at)
|
18
|
+
# merged_nitro_cacheable_index (nitro_cacheable_id,nitro_cacheable_type,nitro_cache_key) UNIQUE
|
19
|
+
#
|
20
|
+
|
21
|
+
class NitroCache < ActiveRecord::Base
|
22
|
+
belongs_to :nitro_cacheable, polymorphic: true
|
23
|
+
belongs_to :nitro_partial
|
24
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: nitro_partials
|
4
|
+
#
|
5
|
+
# id :integer not null, primary key
|
6
|
+
# partial :text
|
7
|
+
# prerender :boolean
|
8
|
+
# expires :string
|
9
|
+
# record_limit :integer
|
10
|
+
# partial_hash :string
|
11
|
+
# cache_keys :jsonb default({}), not null
|
12
|
+
# created_at :datetime not null
|
13
|
+
# updated_at :datetime not null
|
14
|
+
# render_as :string
|
15
|
+
#
|
16
|
+
# Indexes
|
17
|
+
#
|
18
|
+
# index_nitro_partials_on_cache_keys (cache_keys)
|
19
|
+
# index_nitro_partials_on_partial (partial)
|
20
|
+
# index_nitro_partials_on_partial_hash (partial_hash)
|
21
|
+
#
|
22
|
+
|
23
|
+
# cache_keys has very special structure:
|
24
|
+
# { locals: { some_keys_combination } } then hash stored in cache_keys column will have this structure:
|
25
|
+
# { some_keys_combination.to_nitro_cache_key => some_keys_combination } assuming this:
|
26
|
+
# cache_keys.keys is the Array of all cache_keys for selected partial.
|
27
|
+
#
|
28
|
+
#
|
29
|
+
class NitroPartial < ActiveRecord::Base
|
30
|
+
PRERENDER_BATCH = 1000
|
31
|
+
EXPIRES_REGEX = /^\d+\.(day|days|week|weeks|month|months|year|years)\.from_now$/
|
32
|
+
|
33
|
+
has_many :nitro_caches, dependent: :delete_all
|
34
|
+
@@partials_cache = nil
|
35
|
+
@@partials_scopes = {}
|
36
|
+
|
37
|
+
scope :prerender, ->() { where(prerender: true) }
|
38
|
+
|
39
|
+
class << self
|
40
|
+
|
41
|
+
# вытягивает из БД все в переменную класса, чтобы лишний раз не вставать. в процессе этого проверяет не поменялось
|
42
|
+
# ли что-нибудь внутри файлов
|
43
|
+
def partials_cache
|
44
|
+
if @@partials_cache.nil?
|
45
|
+
all.each do |cp|
|
46
|
+
# file was removed
|
47
|
+
if File.exist?("#{Rails.root}/#{cp.partial_path}")
|
48
|
+
# file hash changed
|
49
|
+
if cp.partial_cache_invalid?
|
50
|
+
# we are not update_caches even in prerender. cache updates in special rake
|
51
|
+
cp.remove_caches
|
52
|
+
cp.update_attributes( partial_hash: partial_hash(cp.partial) )
|
53
|
+
end
|
54
|
+
else
|
55
|
+
cp.destroy
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@@partials_cache ||= all.reload.map{ |cp| [cp.partial, cp] }.to_h
|
61
|
+
end
|
62
|
+
|
63
|
+
# scopes for prerender
|
64
|
+
def partials_scopes
|
65
|
+
@@partials_scopes
|
66
|
+
end
|
67
|
+
|
68
|
+
def all_key_combinations( all_locals, partial )
|
69
|
+
return { {partial: partial}.to_nitro_cache_key => {} } if all_locals.blank?
|
70
|
+
# Example for code below:
|
71
|
+
# locals = {:role=>[:user, :admin], :public=>[true, false]}
|
72
|
+
#
|
73
|
+
# transforms into 4 combinations of possible keys:
|
74
|
+
#
|
75
|
+
# key_combinations = [{:public=>true, :role=>:user},
|
76
|
+
# {:public=>true, :role=>:admin},
|
77
|
+
# {:public=>false, :role=>:user},
|
78
|
+
# {:public=>false, :role=>:admin}]
|
79
|
+
#
|
80
|
+
# i.e. we can now do
|
81
|
+
# key_combinations.each do |kc|
|
82
|
+
# render partial: partial, locals: { record_as: record_as }.merge!( kc ) }
|
83
|
+
# end
|
84
|
+
key_combinations = all_locals.keys.map{|key| (all_locals[key].is_a?(Array) ? all_locals[key]: [all_locals[key]]).map{|val| { key => val } } }
|
85
|
+
key_combinations = key_combinations.pop.product(*key_combinations)
|
86
|
+
key_combinations.map!{|key_arr| key_arr.inject({}){|memo, curr| memo.merge(curr) } }
|
87
|
+
key_combinations.map{|locals_set| [locals_set.merge(partial: partial).to_nitro_cache_key, locals_set ]}.to_h
|
88
|
+
end
|
89
|
+
|
90
|
+
# prerender will not start with rails, because it may need large amount of time
|
91
|
+
def add_prerendered_partial( options )
|
92
|
+
existing_partial = partials_cache[options[:partial]] || where( partial: options[:partial] ).first
|
93
|
+
key_combinations = options[:key_combinations] || all_key_combinations( options[:all_locals], options[:partial] )
|
94
|
+
partials_scopes[options[:partial]] = options[:scope]
|
95
|
+
|
96
|
+
raise( ArgumentError, 'expires params must be in form: N.(day[s]|week[s]|month[s]|year[s]).from_now' ) if !options[:expires].blank? || EXPIRES_REGEX === options[:expires].to_s
|
97
|
+
raise( ArgumentError, 'must have scope for prerendering' ) unless options[:scope]
|
98
|
+
|
99
|
+
if existing_partial
|
100
|
+
# all non existed now key combination will be deleted
|
101
|
+
existing_partial.nitro_caches.where.not( nitro_cache_key: key_combinations.keys ).delete_all
|
102
|
+
|
103
|
+
existing_partial.update_attributes(
|
104
|
+
render_as: options[:as],
|
105
|
+
cache_keys: key_combinations,
|
106
|
+
expires: options[:expires].to_s,
|
107
|
+
prerender: true )
|
108
|
+
|
109
|
+
existing_partial
|
110
|
+
else
|
111
|
+
partials_cache[options[:partial]] = create(
|
112
|
+
cache_keys: key_combinations,
|
113
|
+
expires: options[:expires].to_s,
|
114
|
+
prerender: true,
|
115
|
+
render_as: options[:as],
|
116
|
+
partial: options[:partial],
|
117
|
+
partial_hash: partial_hash( options[:partial] ) )
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# получить паршиал или создать ( для обычных кешей без пререндеринга, для пререндеринга, надо вызывать
|
123
|
+
# add_prerendered_partial в модели которой пререндеринг прикрепляется )
|
124
|
+
def get_partial( partial_name, options )
|
125
|
+
# we need where first because rails can be run in parallels so it could be created already elsewhere
|
126
|
+
partials_cache[partial_name] ||= where( partial: partial_name ).first || create( options.with_indifferent_access
|
127
|
+
.merge(
|
128
|
+
partial: partial_name,
|
129
|
+
render_as: options[:as],
|
130
|
+
partial_hash: partial_hash( partial_name ) )
|
131
|
+
.slice(*NitroPartial.column_names) )
|
132
|
+
end
|
133
|
+
|
134
|
+
def partial_hash( partial )
|
135
|
+
Digest::SHA512.file( "#{Rails.root}/#{partial_path(partial)}" ).hexdigest
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def partial_path(partial)
|
140
|
+
# extract partial file name
|
141
|
+
rg = /[^\/]*$/
|
142
|
+
"app/views/#{partial.gsub( partial[rg], "_#{partial[rg]}" )}"
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_scope
|
148
|
+
self.class.partials_scopes[partial]
|
149
|
+
end
|
150
|
+
|
151
|
+
# append_view_path
|
152
|
+
def partial_cache_invalid?
|
153
|
+
self.class.partial_hash(partial) != partial_hash
|
154
|
+
end
|
155
|
+
|
156
|
+
def remove_caches
|
157
|
+
nitro_caches.delete_all
|
158
|
+
end
|
159
|
+
|
160
|
+
def update_caches( progress = nil )
|
161
|
+
(get_scope.count/PRERENDER_BATCH + 1).times do |i|
|
162
|
+
update_cache_for_collection( get_scope.limit(PRERENDER_BATCH).offset(i*PRERENDER_BATCH), progress )
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def update_cache_for_collection( collection, progress = nil )
|
167
|
+
cache_keys.values.each do |locals|
|
168
|
+
progress.try(:inc)
|
169
|
+
ApplicationController.render( assigns: {
|
170
|
+
locals: locals,
|
171
|
+
partial: partial,
|
172
|
+
render_as: render_as,
|
173
|
+
rel: collection },
|
174
|
+
inline: '<% db_cache_collection( straight_cache: false, collection: @rel, partial: @partial, as: @render_as, locals: @locals )%>' )
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def partial_path
|
179
|
+
self.class.partial_path(partial)
|
180
|
+
end
|
181
|
+
|
182
|
+
#на старте системы сохраняем все кеши в локальную структуру.
|
183
|
+
partials_cache
|
184
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'nitro_pg_cache/engine'
|
2
|
+
|
3
|
+
require 'nitro_pg_cache/model_ext'
|
4
|
+
require 'nitro_pg_cache/viewer_ext'
|
5
|
+
require 'nitro_pg_cache/acts_as_nitro_cacheable'
|
6
|
+
|
7
|
+
ActiveSupport.on_load(:active_record) do
|
8
|
+
extend NitroPgCache::NitroCacheable # adds act_as_nitro_cachable
|
9
|
+
end
|
10
|
+
|
11
|
+
ActiveSupport.on_load(:action_view) do
|
12
|
+
include NitroPgCache::ViewerExt
|
13
|
+
end
|
14
|
+
|
15
|
+
class Hash
|
16
|
+
# little lazy hackery, retrieve_cache_key - private, so technically it's wrong, but why the heck it's private?
|
17
|
+
# retrieve_cache_key doesn't respect order so I force key sorting
|
18
|
+
def to_nitro_cache_key
|
19
|
+
"#{self[:partial]}_#{ActiveSupport::Cache.send( :retrieve_cache_key, self[:cache_by] )}_#{ self[:locals] && self[:locals].keys.sort.map{|key| ActiveSupport::Cache.send( :retrieve_cache_key, self[:locals][key] ) }.join("_")}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require_dependency 'models/nitro_cache'
|
24
|
+
require_dependency 'models/nitro_partial'
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module NitroPgCache
|
2
|
+
module ModelExt
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
#
|
7
|
+
after_save :clear_nitro_cache
|
8
|
+
after_touch :clear_nitro_cache
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# since it will be wrapped in scoping we can implement it as model class method not the relation methods
|
14
|
+
def aggregate_collection(locals_key)
|
15
|
+
# we can't use NitroCache.where( nitro_cache_key: nitro_cache_key, nitro_cacheable: self )
|
16
|
+
# because we will lose collection order!
|
17
|
+
# why use connection.execute instead of doing self.select also because of an order. if you using some order on your scope
|
18
|
+
# than columns you using to order must appear in the GROUP BY clause or be used in an aggregate function or you will get an error
|
19
|
+
connection.execute( <<AGGREGATE_STR
|
20
|
+
SELECT string_agg( nitro_cached_value, '') as str_agg,
|
21
|
+
bool_or( nitro_cached_value is null ) as has_nulls
|
22
|
+
FROM (#{self.select_nitro_cache.outer_join_partials(locals_key).to_sql}) as db_cache
|
23
|
+
AGGREGATE_STR
|
24
|
+
)[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def select_all_if_empty
|
28
|
+
all.select_values.blank? ? select( "#{table_name}.*" ) : all
|
29
|
+
end
|
30
|
+
|
31
|
+
def nitro_cache_bulk_insert( options )
|
32
|
+
return if options[:caches].blank?
|
33
|
+
# "nitro_cache_key", "nitro_cacheable_type", "viewed_at" - all same for entire collection
|
34
|
+
last_tail = "'#{options[:nitro_cache_key]}', '#{self}', '#{Time.now}', '#{options[:nitro_partial_id]}'"
|
35
|
+
tail = ", #{last_tail} ), ("
|
36
|
+
# this is practically Values String without last tail and external enclosing brackets
|
37
|
+
values = options[:caches].map{ |id_value| "#{id_value[0]}, $$#{id_value[1]}$$" }.join( tail )
|
38
|
+
sql = <<INSERT
|
39
|
+
INSERT INTO nitro_caches ("nitro_cacheable_id", "nitro_cached_value", "nitro_cache_key", "nitro_cacheable_type", "viewed_at", "nitro_partial_id")
|
40
|
+
VALUES ( #{values}, #{last_tail} )
|
41
|
+
ON CONFLICT (nitro_cacheable_id, nitro_cacheable_type, nitro_cache_key)
|
42
|
+
DO UPDATE SET nitro_cached_value = EXCLUDED.nitro_cached_value
|
43
|
+
INSERT
|
44
|
+
ActiveRecord::Base.connection.execute( sql )
|
45
|
+
end
|
46
|
+
|
47
|
+
def select_nitro_cache
|
48
|
+
select( ['nitro_caches.nitro_cached_value as nitro_cached_value'] )
|
49
|
+
end
|
50
|
+
|
51
|
+
#prepare for aggregation
|
52
|
+
def outer_join_partials(cache_key)
|
53
|
+
joins(<<JOIN
|
54
|
+
LEFT OUTER JOIN "nitro_caches"
|
55
|
+
ON "nitro_caches"."nitro_cacheable_id" = "#{table_name}"."id"
|
56
|
+
AND "nitro_caches"."nitro_cacheable_type" = '#{base_class.to_s}'
|
57
|
+
AND "nitro_caches"."nitro_cache_key" = '#{cache_key}'
|
58
|
+
JOIN
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# add partial to prendering
|
63
|
+
# options: { partial: , all_locals:, record_as: , partial: , scope: }
|
64
|
+
# partial: path to partial from Rails.root
|
65
|
+
# all_locals: array of hashes of possible cache_keys|locals_name, {locals_name: [value1, value2], locals_name: [ value3, value4 ]} see example below
|
66
|
+
# prerender: true/false, flag that indicates does or doesn't this partial will be prerendered when a record changes, it's
|
67
|
+
# expires: [/^\d+\.(day|days|week|weeks|month|months|year|years)\.from_now$/] as string N.(day[s]|week[s]|month[s]|year[s]).from_now
|
68
|
+
# record_as: :symbol , if we prerendering than we pass locals
|
69
|
+
# scope: scope wich is used for prerendering purpose, you may want to add some includes to it, same as includes in your controller action
|
70
|
+
#
|
71
|
+
# Example:
|
72
|
+
# prerender_cache_partial(
|
73
|
+
# partial: 'app/views/products/product'
|
74
|
+
# locals: {:role=>[:user, :admin], :public=>[true, false]},
|
75
|
+
# scope: Model.where(created_at: -6.month.from_now..Time.now)
|
76
|
+
# )
|
77
|
+
#todo перед eval(expires) все равно сделать проверку. а то иначе можно положить исполняемую строку в БД и потом вынудить исполнится на прогоне )
|
78
|
+
|
79
|
+
def add_prerender_partial(options)
|
80
|
+
p_partial = NitroPartial.add_prerendered_partial( { scope: all }.merge(options) )
|
81
|
+
after_commit { p_partial.update_cache_for_collection([self]) }
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
def clear_nitro_cache
|
87
|
+
NitroCache.where(nitro_cacheable: self).delete_all
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module NitroPgCache
|
2
|
+
module ViewerExt
|
3
|
+
def db_cache( item )
|
4
|
+
if @current_cache_aggregator[:reverse] || !item.try(:nitro_cached_value)
|
5
|
+
@current_cache_aggregator[:caches] << [item.id, yield]
|
6
|
+
else
|
7
|
+
safe_concat( item.nitro_cached_value )
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# I decided to keep both solutions reverse and straight. reverse is faster,
|
12
|
+
# but straight is more srtaightforward :) and may be more stable, especially with DB sharding
|
13
|
+
# also when using prerendering speed is the same and stability is preferable
|
14
|
+
def db_cache_collection(options)
|
15
|
+
|
16
|
+
if options[:collection].is_a?(ActiveRecord::Relation)
|
17
|
+
|
18
|
+
whole_cache_key = options[:collection].try(:cache_key)
|
19
|
+
cached_result = NitroCache.where( nitro_cache_key: whole_cache_key, nitro_cacheable_id: nil, nitro_cacheable_type: nil).first
|
20
|
+
|
21
|
+
if cached_result
|
22
|
+
cached_result.update_attributes(viewed_at: Time.now)
|
23
|
+
return cached_result.try(:nitro_cached_value).try(:html_safe)
|
24
|
+
end
|
25
|
+
|
26
|
+
render_result = options[:straight_cache] ? db_cache_collection_s( options ) : db_cache_collection_r( options )
|
27
|
+
|
28
|
+
# why not NitroCache.create( nitro_cached_value: render_result, nitro_cache_key: whole_cache_key, nitro_cacheable_id: nil, nitro_cacheable_type: nil) &
|
29
|
+
# because on large collections raw insert is 1.2 faster, i.e. same speed as memcache.
|
30
|
+
NitroCache.connection.execute( <<INSERT_STR
|
31
|
+
INSERT INTO "nitro_caches" ("nitro_cached_value", "nitro_cache_key", "nitro_partial_id", "viewed_at" )
|
32
|
+
VALUES ('#{render_result}', '#{whole_cache_key}', '#{@current_cache_aggregator[:nitro_partial_id]}', '#{Time.now}')
|
33
|
+
INSERT_STR
|
34
|
+
)
|
35
|
+
render_result
|
36
|
+
else
|
37
|
+
db_cache_array( options )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# It's used ONLY for prerender purpose of single element collection: [elem]
|
42
|
+
def db_cache_array( options )
|
43
|
+
collection = options[:collection]
|
44
|
+
# nitro_cache_key - same across all collection
|
45
|
+
nitro_cache_key = options.to_nitro_cache_key
|
46
|
+
|
47
|
+
# this is place where NitroPartial creates new partials info in DB
|
48
|
+
@current_cache_aggregator = { nitro_partial_id: NitroPartial.get_partial( options[:partial], options ).id,
|
49
|
+
nitro_cache_key: nitro_cache_key,
|
50
|
+
reverse: false,
|
51
|
+
caches: [] }
|
52
|
+
|
53
|
+
result = render( partial: options[:partial],
|
54
|
+
collection: collection,
|
55
|
+
as: options[:as],
|
56
|
+
locals: {
|
57
|
+
locals: options[:locals]
|
58
|
+
}
|
59
|
+
)
|
60
|
+
(collection.try(:first) ? collection.try(:first).class : collection.klass).nitro_cache_bulk_insert( @current_cache_aggregator )
|
61
|
+
|
62
|
+
NitroCache.where( nitro_cacheable: collection, nitro_cache_key: nitro_cache_key ).update_all(viewed_at: Time.now)
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
# cache_key generated with cache_by and locals
|
67
|
+
# straight cache. same logic as usual cache.
|
68
|
+
def db_cache_collection_s( options )
|
69
|
+
collection = options[:collection]
|
70
|
+
# nitro_cache_key - same across all collection
|
71
|
+
nitro_cache_key = options.to_nitro_cache_key
|
72
|
+
result = ""
|
73
|
+
|
74
|
+
aggr_result = collection.aggregate_collection( nitro_cache_key )
|
75
|
+
if aggr_result['has_nulls'] == 't'
|
76
|
+
# this is place where NitroPartial creates new partials info in DB
|
77
|
+
@current_cache_aggregator = { nitro_partial_id: NitroPartial.get_partial( options[:partial], options ).id,
|
78
|
+
nitro_cache_key: nitro_cache_key,
|
79
|
+
reverse: false,
|
80
|
+
caches: [] }
|
81
|
+
|
82
|
+
result = render( partial: options[:partial],
|
83
|
+
#because of select nitro_cached_value we need to add select( 'all.*' )
|
84
|
+
collection: collection.select_all_if_empty
|
85
|
+
.select_nitro_cache
|
86
|
+
.outer_join_partials(nitro_cache_key),
|
87
|
+
as: options[:as],
|
88
|
+
locals: {
|
89
|
+
locals: options[:locals]
|
90
|
+
}
|
91
|
+
)
|
92
|
+
# Нужно массив все таки переделать в хеш на случай повторений в коллекции
|
93
|
+
collection.klass.nitro_cache_bulk_insert( @current_cache_aggregator )
|
94
|
+
|
95
|
+
else
|
96
|
+
result = aggr_result['str_agg'].try(:html_safe)
|
97
|
+
end
|
98
|
+
|
99
|
+
#NitroCache.where( nitro_cacheable: collection, nitro_cache_key: nitro_cache_key) breaks on complex collections
|
100
|
+
NitroCache.where( nitro_cacheable_type: collection.base_class,
|
101
|
+
nitro_cacheable_id: collection.base_class
|
102
|
+
.from( "(#{collection.to_sql }) #{collection.table_name}" ).select(:id),
|
103
|
+
nitro_cache_key: nitro_cache_key )
|
104
|
+
.update_all(viewed_at: Time.now)
|
105
|
+
|
106
|
+
# alternative way to
|
107
|
+
#ActiveRecord::Base.connection.execute( <<UPDATE_SQL
|
108
|
+
# UPDATE "nitro_cahes"
|
109
|
+
# SET "viewed_at" = '#{Time.now}'
|
110
|
+
# WHERE "nitro_partials"."id" IN (
|
111
|
+
# SELECT "nitro_caches"."id" FROM (#{collection.outer_join_partials(nitro_cache_key).unscope(:select).select('"nitro_caches"."id"').to_sql}) as db_cache_partials )
|
112
|
+
# UPDATE_SQL
|
113
|
+
# )
|
114
|
+
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# 'reverse' cache: we first getting only noncached records, renders them, cache, and after that
|
120
|
+
# aggregate whole collection from DB
|
121
|
+
|
122
|
+
# вариант с кеш реверсом, сначала достаем те которые не отрисованы/закешированы,
|
123
|
+
# отрисовываем их, и после этого делаем без всяких извратов аггрегацию
|
124
|
+
def db_cache_collection_r( options )
|
125
|
+
collection = options[:collection]
|
126
|
+
# nitro_cache_key - same across all collection
|
127
|
+
nitro_cache_key = options.to_nitro_cache_key
|
128
|
+
|
129
|
+
@current_cache_aggregator = { nitro_partial_id: NitroPartial.get_partial( options[:partial], options ).id,
|
130
|
+
nitro_cache_key: nitro_cache_key,
|
131
|
+
reverse: true,
|
132
|
+
caches: [] }
|
133
|
+
# some how rails can't manage it through collection, so we downgrade it to collection.base_class
|
134
|
+
# but it must go unscoped, default_scopes will be added from original collection
|
135
|
+
render_collection = collection.base_class.unscoped
|
136
|
+
collection.values.slice(:joins, :references, :includes).each{|key, value| render_collection = render_collection.send("#{key}", value) }
|
137
|
+
|
138
|
+
render( partial: options[:partial],
|
139
|
+
collection: render_collection.from( "(#{collection.outer_join_partials(nitro_cache_key)
|
140
|
+
.select_all_if_empty
|
141
|
+
.select_nitro_cache.to_sql }) #{collection.table_name}" )
|
142
|
+
.where(nitro_cached_value: nil),
|
143
|
+
as: options[:as],
|
144
|
+
locals: {
|
145
|
+
locals: options[:locals]
|
146
|
+
}
|
147
|
+
)
|
148
|
+
|
149
|
+
collection.klass.nitro_cache_bulk_insert( @current_cache_aggregator ) unless @current_cache_aggregator[:caches].blank?
|
150
|
+
|
151
|
+
# todo SHARD!
|
152
|
+
# if @current_cache_aggregator[:caches].blank? than we can ask any shard, else only master shard
|
153
|
+
|
154
|
+
#NitroCache.where( nitro_cacheable: collection, nitro_cache_key: nitro_cache_key ) - breaks :(
|
155
|
+
NitroCache.where( nitro_cacheable_type: collection.base_class,
|
156
|
+
nitro_cacheable_id: collection.base_class.unscoped
|
157
|
+
.from( "(#{collection.to_sql }) #{collection.table_name}" ).select(:id),
|
158
|
+
nitro_cache_key: nitro_cache_key ).update_all(viewed_at: Time.now)
|
159
|
+
|
160
|
+
collection.aggregate_collection( nitro_cache_key )['str_agg'].try(:html_safe)
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nitro_pg_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.rc1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- alekseyl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
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: rails_select_on_includes
|
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: pg_cache_key
|
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: "PostgreSQL fast cache. Faster than memcache+dalli on same machine.\n
|
70
|
+
\ Features: 'instant' reordering cached collection and subcollection
|
71
|
+
rendering, prerendering, 2-3 faster rendering of partially cached collection "
|
72
|
+
email:
|
73
|
+
- leshchuk@gmail.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- MIT-LICENSE
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- config/initializers/inflections.rb
|
82
|
+
- db/migrate/1_add_nitro_cache_tables.rb
|
83
|
+
- lib/models/nitro_cache.rb
|
84
|
+
- lib/models/nitro_partial.rb
|
85
|
+
- lib/nitro_pg_cache.rb
|
86
|
+
- lib/nitro_pg_cache/acts_as_nitro_cacheable.rb
|
87
|
+
- lib/nitro_pg_cache/engine.rb
|
88
|
+
- lib/nitro_pg_cache/model_ext.rb
|
89
|
+
- lib/nitro_pg_cache/version.rb
|
90
|
+
- lib/nitro_pg_cache/viewer_ext.rb
|
91
|
+
homepage: ''
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 1.3.1
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.5.1
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: 'PostgreSQL fast cache. Faster than memcache+dalli on same machine. Features:
|
115
|
+
''instant'' reordering cached collection and subcollection rendering, prerendering,
|
116
|
+
2-3 faster rendering of partially cached collection'
|
117
|
+
test_files: []
|