nitro_pg_cache 0.1.rc1
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/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: []
|