| 28 | A new table in the database stores the cache identifiers, along with the current generation number and possibly the time of the last invalidation (for timed cache invalidation). The schema would be something like: |
| 29 | {{{ |
| 30 | Table('cache', key='id')[ |
| 31 | Column('id'), |
| 32 | Column('generation', type='int'), |
| 33 | Column('time', time='int'), |
| 34 | ] |
| 35 | }}} |
| 36 | |
| 37 | So how is the cache used? |
| 38 | |
| 39 | * '''HTTP request''': At the beginning of every HTTP request, the complete `cache` table is read into memory. This provides the `CacheManager` with the current state of the database data. Timed invalidation could also be done at this point, by dropping cached data that is too old. |
| 40 | |
| 41 | * '''Retrieval of cached data''': The `CacheManager` can be queried for a reference to a cache. At this point, it checks if the generation number of the cached data matches the number read at the start of the HTTP request. If it does, the cached data is simply returned. Otherwise, the cached data is discarded, the retrieval function is called to populate the cache with fresh data, and the data is returned. |
| 42 | |
| 43 | * '''Invalidation of cached data''': Invalidation of cached data is done explicitly after updating the database by incrementing the generation number for the cache in the `cache` table, in the same transaction as the data update, and invalidating the currently cached data in the `CacheManager`. |
| 44 | |
| 45 | Pros: |
| 46 | * Caches are managed in a single place, and the cache logic is implemented once and for all. This should avoid bugs due to re-implementing cache logic for every individual cache. |
| 47 | * Cached data is consistent for the duration of an HTTP request. |
| 48 | * Caches can be made fine-grained. For example, it may be possible to use separate caches for the values of every ticket field (not sure we want that, though). Invalidation is fine-grained as well. |
| 49 | |
| 50 | Cons: |
| 51 | * One additional database query per HTTP request. I don't know how much impact this can have, but I would expect this to be negligible, as the `cache` table should never grow past a few dozen rows. |
| 52 | * Caches must be invalidated explicitly. The same drawback applies to the current situation, so nothing is lost there. |
| 53 | |
| 54 | Open questions: |
| 55 | * This strategy should work well in a multi-process scenario. In a multi-thread scenario, proper locking must ensure that cached data is not modified during a request. It may be possible to use thread-local storage to ensure that a single request has a consistent view of the cache, even if a second thread invalidates the cache. |
| 56 | |
| 57 | Comments and improvements are welcome. If this approach sounds reasonable, I'd like to do a prototype implementation and apply it to a few locations (the wiki page cache and ticket fields). |