nitro_pg_cache 0.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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: []