A decorator which wraps HistoryAccessor method calls to catch errors from a corrupt SQLite database, move the old database out of the way, and create a new one. We avoid clobbering larger databases because this may be triggered due to filesystem issues, not just a corrupt file.
(f, self, *a, **kw)
| 113 | |
| 114 | @decorator |
| 115 | def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def] |
| 116 | """A decorator which wraps HistoryAccessor method calls to catch errors from |
| 117 | a corrupt SQLite database, move the old database out of the way, and create |
| 118 | a new one. |
| 119 | |
| 120 | We avoid clobbering larger databases because this may be triggered due to filesystem issues, |
| 121 | not just a corrupt file. |
| 122 | """ |
| 123 | try: |
| 124 | return f(self, *a, **kw) |
| 125 | except (DatabaseError, OperationalError) as e: |
| 126 | self._corrupt_db_counter += 1 |
| 127 | self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e) |
| 128 | if self.hist_file != ":memory:": |
| 129 | if self._corrupt_db_counter > self._corrupt_db_limit: |
| 130 | self.hist_file = ":memory:" |
| 131 | self.log.error( |
| 132 | "Failed to load history too many times, history will not be saved." |
| 133 | ) |
| 134 | elif self.hist_file.is_file(): |
| 135 | # move the file out of the way |
| 136 | base = str(self.hist_file.parent / self.hist_file.stem) |
| 137 | ext = self.hist_file.suffix |
| 138 | size = self.hist_file.stat().st_size |
| 139 | if size >= _SAVE_DB_SIZE: |
| 140 | # if there's significant content, avoid clobbering |
| 141 | now = ( |
| 142 | datetime.datetime.now(datetime.timezone.utc) |
| 143 | .isoformat() |
| 144 | .replace(":", ".") |
| 145 | ) |
| 146 | newpath = base + "-corrupt-" + now + ext |
| 147 | # don't clobber previous corrupt backups |
| 148 | for i in range(100): |
| 149 | if not Path(newpath).exists(): |
| 150 | break |
| 151 | else: |
| 152 | newpath = base + "-corrupt-" + now + ("-%i" % i) + ext |
| 153 | else: |
| 154 | # not much content, possibly empty; don't worry about clobbering |
| 155 | # maybe we should just delete it? |
| 156 | newpath = base + "-corrupt" + ext |
| 157 | self.hist_file.rename(newpath) |
| 158 | self.log.error( |
| 159 | "History file was moved to %s and a new file created.", newpath |
| 160 | ) |
| 161 | self.init_db() |
| 162 | return [] |
| 163 | else: |
| 164 | # Failed with :memory:, something serious is wrong |
| 165 | raise |
| 166 | |
| 167 | |
| 168 | class HistoryAccessorBase(LoggingConfigurable): |