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 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,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.irregular 'cache', 'caches'
3
+ end
@@ -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,8 @@
1
+ module NitroPgCache
2
+ module NitroCacheable
3
+ def acts_as_nitro_cacheable
4
+ include NitroPgCache::ModelExt
5
+ has_many :nitro_caches, as: :nitro_cacheable
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module NitroPgCache
2
+ class Engine < ::Rails::Engine
3
+ #isolate_namespace NitroPgCache
4
+ end
5
+ end
@@ -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,3 @@
1
+ module NitroPgCache
2
+ VERSION = '0.1.rc1'
3
+ 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: []