Edgewall Software

Changes between Version 16 and Version 17 of TracDev/Proposals/CacheInvalidation


Ignore:
Timestamp:
Mar 29, 2009, 11:03:13 PM (15 years ago)
Author:
Remy Blank
Comment:

Reorganized discussion section.

Legend:

Unmodified
Added
Removed
Modified
  • TracDev/Proposals/CacheInvalidation

    v16 v17  
    100100'''Comments and testing are very welcome'''. The implementation is quite complete, except for the missing database upgrade module. I have only tested with several concurrent `tracd` instances so far.
    101101
    102 ==== cboos' feedback ====
     102==== cboos: feedback ====
    103103It's really nice, I like it a lot!
    104104
    105105When reviewing the code, I think I've detected some possible issues in `CacheManager.get`.
    106106 - in case there are multiple "out-of-date" threads, each might trigger a retrieval. An improvement would be to check if the `CacheManager` already has a "newer" generation.
     107
    107108 - in the locked section, if the generation increases after the cached value retrieval and before the fetch of the latest generation, the `CacheManager` may think it is up to date yet have old data.
    108109Those are admittedly corner cases, I hope I have not missed more important issues while focusing on that ;-) See the first patch attachment:cache-manager_get-corner-cases.patch.
     
    111112I've also added more documentation to the decorators and changed the order of the definitions in a top-down way (first the decorators, then the descriptors, ending with the proxy class), as I think it's easier to understand that way.
    112113
    113 ==== rblank ====
     114==== rblank: ====
    114115Thanks for the improvement ideas, I'll integrate them shortly.
    115116 - I'm not sure the DB race condition you describe can actually happen. At least with SQLite, issuing a `SELECT` sets the lock state to `SHARED`, which disallows writes, so it should not be possible to increase the generation between the data retrieval and fetching the generation. I don't know how this behaves with other databases, though. Maybe it's just safer to fetch the generation first.
    116     * SELECT statements don't start a transaction in PySqlite, as opposed to the other DML statements. So in my understanding, each retrieval is "atomic" and I think there can indeed be a race condition between the SELECT(s) done for data retrieval and the SELECT for fetching the generation.
    117       As this picked my curiosity, I tried to see how multiple SELECTs could be done within a single transaction, and this is indeed possible, but a bit heavyweight: see e.g. pysqlite:IdRange, look for `def get_id_range`. So I think it's better to simply cope with the race.
    118117     
    119118 - You're right about automating the cache key generation. I didn't want to do it first, because renaming a module or class would have changed the key. But we're not going to rename them, and even if we do, it will only leave an orphaned row in the `cache` table, so it's no big deal. Your patch proposed `{module}.{function}` as the key, I'd like to make it `{module}.{class}.{function}`.
    120    * simple oversight on my part; sure, make the class name part of the key.
    121119
    122120 - If the keys are auto-generated, the decorators don't need any arguments anymore. This allows simplifying them even more by dropping the `cached()` and `cached_value()` functions, and calling the descriptor classes `cached` and `cached_value` directly.
    123    * great, I didn't know one can do that
    124 
    125 ==== !CacheManager used in non-Component classes ====
    126 
     121
     122==== cboos: ====
     123 - SELECT statements don't start a transaction in PySqlite, as opposed to the other DML statements. So in my understanding, each retrieval is "atomic" and I think there can indeed be a race condition between the SELECT(s) done for data retrieval and the SELECT for fetching the generation.
     124   As this picked my curiosity, I tried to see how multiple SELECTs could be done within a single transaction, and this is indeed possible, but a bit heavyweight: see e.g. pysqlite:IdRange, look for `def get_id_range`. So I think it's better to simply cope with the race.
     125
     126 - simple oversight on my part; sure, make the class name part of the key.
     127
     128 - great, I didn't know one can do that
     129
     130==== cboos: !CacheManager used in non-Component classes ====
    127131When thinking about how to use the !CacheManager for getting the `youngest_rev` information from !CachedRepository, I saw two additional problems:
    128132 - we can't use the decorators here, as the !CachedRepository is not a Component (and shouldn't be, as we may have many instances per environment)
     133
    129134 - so far we have avoided propagating the `env` to the `CachedRepository`. I think we can no longer afford to do this, if we want to access the !CacheManager conveniently. Having the `env` available would also simplify the `getdb` stuff.
    130135
     
    141146Do you see a better way to do it?
    142147
    143  - Yes, by instantiating `CacheProxy` in the constructor and storing it as an instance attribute. This gives it the same interface as if `@cached` was used.
     148==== rblank: ====
     149Yes, by instantiating `CacheProxy` in the constructor and storing it as an instance attribute. This gives it the same interface as if `@cached` was used.
    144150{{{
    145151#!python
    146152    self.metadata = CacheProxy('CachedRepository.metadata:' + self.name, self.get_metadata, env)
    147153}}}
    148    This does indeed require `env`, and changing that will make the `CachedRepository` unit tests a bit more complicated :-/
    149 
    150 ==== Update with feedback ====
    151 
     154This does indeed require `env`, and changing that will make the `CachedRepository` unit tests a bit more complicated :-/
     155
     156==== rblank: Update with feedback ====
    152157The attachment:cache-manager-r7989.patch is an updated patch which should take into account all corner cases described above. Cache keys are now auto-generated from the module, class and attribute name. I have also added the database upgrade code, so the `db_version` is now 22.
    153158
    154159Are there any other issues that should be considered? If not, the next step would be to plan the integration into trunk. Are there any special considerations when upgrading the database version? What else (other than committing) must be done?
    155160
     161==== cboos: feedback ====
    156162 - Last round of feedback:
    157163   - The API documentation should also mention that `cached` and `cached_value`
    158      must be used within Component sub-classes, and what the `receiver` method
     164     must be used within Component sub-classes, and what the `retriever` method
    159165     should look like
    160      - Will do.
    161166   - in `CacheManager.invalidate`, the `SELECT ... `, `if fetchone UPDATE`, `else INSERT`
    162167     is not thread-safe (again for the same reason that a SELECT doesn't start a transaction)
    163      so we should rather do `try INSERT `, `except UPDATE`.
    164      - That's what I tried first, but the error due to the `INSERT` rolled back the whole transaction. I'll have to find a way to do this in a single statement.
    165        - Hm right, that can be problematic. So what about this:
    166          {{{
     168     so we should rather do `try INSERT`, `except UPDATE`.
     169
     170 Both points are minor and could be done on trunk.
     171
     172 - What to do next?
     173   - maybe send a mail on Trac-dev (in the same thread you started a while ago)
     174     saying the topic work is done and ask if anyone has some extra feedback to give
     175   - after the commit, warn loudly on the milestone:0.12, on the ["TracDev/ReleaseNotes/0.12"]
     176     and ["0.12/TracUpgrade"] pages that the DB version has increased.
     177     It's not that it's problematic to do the upgrade, it's rather because
     178     it's inconvenient to downgrade. As long as we keep the DB version compatible,
     179     users can eventually go back and forth between trunk and 0.11-stable.
     180     Once they did an upgrade, it's not that convenient anymore (but still
     181     relatively easy to do in this specific case, of course).
     182
     183 - We could also think about adding some tests for this, though that might be more involved.
     184
     185==== rblank: ====
     186 - Replies to last round of feedback:
     187   - Will do.
     188   - That's what I tried first, but the error due to the `INSERT` rolled back the whole transaction. I'll have to find a way to do this in a single statement.
     189
     190==== cboos: ====
     191Hm right, that can be problematic. So what about this:
     192{{{
    167193#!python
    168194    cursor.execute("SELECT generation FROM cache WHERE key=%s",
     
    178204            cursor.execute("UPDATE cache SET generation=generation+1"
    179205                           "WHERE key=%s", (key,))
    180            }}}
    181          If we were in a transaction, then I suppose the SELECT/INSERT
    182          sequence can't fail. Conversely, if it fails, then we were
    183          ''not'' in a transaction, and we can follow-up with an UPDATE
    184          to recover from the failed INSERT.
    185        - That could work, yes. How about this:
    186          {{{
     206}}}
     207If we were in a transaction, then I suppose the SELECT/INSERT
     208sequence can't fail. Conversely, if it fails, then we were
     209''not'' in a transaction, and we can follow-up with an UPDATE
     210to recover from the failed INSERT.
     211
     212==== rblank: Alternative for atomic UPSERT ====
     213That could work, yes. How about this:
     214{{{
    187215#!python
    188216    cursor.execute("UPDATE cache SET generation=generation+1 "
     
    191219    if not cursor.fetchone():
    192220        cursor.execute("INSERT INTO cache VALUES (%s, %s)", (key, 0))
    193          }}}
    194          If the row already exists, it is updated, the `SELECT` returns a row and we're done.
    195          If not, the `UPDATE` does nothing except starting a transaction (or we may already be in a transaction), the `SELECT`
    196          doesn't return any rows, and we do the `INSERT` in the same transaction. Doesn't the `UPDATE` even return the number
    197          of altered rows? That would remove the need for a separate `SELECT`. I'm not sure though that the `UPDATE` starts a transaction
    198          if no rows are altered. We may have to use a dummy row that is always updated in addition to the desired row.
    199    Both points are minor and could be done on trunk.
    200  - What to do next?
    201    - maybe send a mail on Trac-dev (in the same thread you started a while ago)
    202      saying the topic work is done and ask if anyone has some extra feedback to give
    203    - after the commit, warn loudly on the milestone:0.12, on the ["TracDev/ReleaseNotes/0.12"]
    204      and ["0.12/TracUpgrade"] pages that the DB version has increased.
    205      It's not that it's problematic to do the upgrade, it's rather because
    206      it's inconvenient to downgrade. As long as we keep the DB version compatible,
    207      users can eventually go back and forth between trunk and 0.11-stable.
    208      Once they did an upgrade, it's not that convenient anymore (but still
    209      relatively easy to do in this specific case, of course).
    210  - We could also think about adding some tests for this, though that might be more involved.
     221}}}
     222If the row already exists, it is updated, the `SELECT` returns a row and we're done.
     223
     224If not, the `UPDATE` does nothing except starting a transaction (or we may already be in a transaction), the `SELECT`
     225doesn't return any rows, and we do the `INSERT` in the same transaction. Doesn't the `UPDATE` even return the number
     226of altered rows? That would void the need for a separate `SELECT`. I'm not sure though that the `UPDATE` starts a
     227transaction if no rows are altered. We may have to use a dummy row that is always updated in addition to the desired row.
     228
    211229
    212230== Idea 2: Cache control ==