Changes between Version 16 and Version 17 of TracDev/Proposals/CacheInvalidation
- Timestamp:
- Mar 29, 2009, 11:03:13 PM (15 years ago)
Legend:
- Unmodified
- Added
- Removed
- Modified
-
TracDev/Proposals/CacheInvalidation
v16 v17 100 100 '''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. 101 101 102 ==== cboos 'feedback ====102 ==== cboos: feedback ==== 103 103 It's really nice, I like it a lot! 104 104 105 105 When reviewing the code, I think I've detected some possible issues in `CacheManager.get`. 106 106 - 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 107 108 - 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. 108 109 Those 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. … … 111 112 I'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. 112 113 113 ==== rblank ====114 ==== rblank: ==== 114 115 Thanks for the improvement ideas, I'll integrate them shortly. 115 116 - 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.118 117 119 118 - 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.121 119 122 120 - 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 ==== 127 131 When thinking about how to use the !CacheManager for getting the `youngest_rev` information from !CachedRepository, I saw two additional problems: 128 132 - 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 129 134 - 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. 130 135 … … 141 146 Do you see a better way to do it? 142 147 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: ==== 149 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. 144 150 {{{ 145 151 #!python 146 152 self.metadata = CacheProxy('CachedRepository.metadata:' + self.name, self.get_metadata, env) 147 153 }}} 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 154 This does indeed require `env`, and changing that will make the `CachedRepository` unit tests a bit more complicated :-/ 155 156 ==== rblank: Update with feedback ==== 152 157 The 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. 153 158 154 159 Are 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? 155 160 161 ==== cboos: feedback ==== 156 162 - Last round of feedback: 157 163 - The API documentation should also mention that `cached` and `cached_value` 158 must be used within Component sub-classes, and what the `re ceiver` method164 must be used within Component sub-classes, and what the `retriever` method 159 165 should look like 160 - Will do.161 166 - in `CacheManager.invalidate`, the `SELECT ... `, `if fetchone UPDATE`, `else INSERT` 162 167 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: ==== 191 Hm right, that can be problematic. So what about this: 192 {{{ 167 193 #!python 168 194 cursor.execute("SELECT generation FROM cache WHERE key=%s", … … 178 204 cursor.execute("UPDATE cache SET generation=generation+1" 179 205 "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 }}} 207 If we were in a transaction, then I suppose the SELECT/INSERT 208 sequence can't fail. Conversely, if it fails, then we were 209 ''not'' in a transaction, and we can follow-up with an UPDATE 210 to recover from the failed INSERT. 211 212 ==== rblank: Alternative for atomic UPSERT ==== 213 That could work, yes. How about this: 214 {{{ 187 215 #!python 188 216 cursor.execute("UPDATE cache SET generation=generation+1 " … … 191 219 if not cursor.fetchone(): 192 220 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 }}} 222 If the row already exists, it is updated, the `SELECT` returns a row and we're done. 223 224 If not, the `UPDATE` does nothing except starting a transaction (or we may already be in a transaction), the `SELECT` 225 doesn't return any rows, and we do the `INSERT` in the same transaction. Doesn't the `UPDATE` even return the number 226 of altered rows? That would void the need for a separate `SELECT`. I'm not sure though that the `UPDATE` starts a 227 transaction if no rows are altered. We may have to use a dummy row that is always updated in addition to the desired row. 228 211 229 212 230 == Idea 2: Cache control ==