13
13
# See the License for the specific language governing permissions and
14
14
# limitations under the License.
15
15
import abc
16
+ import logging
16
17
import threading
17
18
from typing import Callable , List , Optional
18
19
19
- from synapse .storage .engines import BaseDatabaseEngine , PostgresEngine
20
- from synapse .storage .types import Cursor
20
+ from synapse .storage .engines import (
21
+ BaseDatabaseEngine ,
22
+ IncorrectDatabaseSetup ,
23
+ PostgresEngine ,
24
+ )
25
+ from synapse .storage .types import Connection , Cursor
26
+
27
+ logger = logging .getLogger (__name__ )
28
+
29
+
30
+ _INCONSISTENT_SEQUENCE_ERROR = """
31
+ Postgres sequence '%(seq)s' is inconsistent with associated
32
+ table '%(table)s'. This can happen if Synapse has been downgraded and
33
+ then upgraded again, or due to a bad migration.
34
+
35
+ To fix this error, shut down Synapse (including any and all workers)
36
+ and run the following SQL:
37
+
38
+ SELECT setval('%(seq)s', (
39
+ %(max_id_sql)s
40
+ ));
41
+
42
+ See docs/postgres.md for more information.
43
+ """
21
44
22
45
23
46
class SequenceGenerator (metaclass = abc .ABCMeta ):
@@ -28,6 +51,19 @@ def get_next_id_txn(self, txn: Cursor) -> int:
28
51
"""Gets the next ID in the sequence"""
29
52
...
30
53
54
+ @abc .abstractmethod
55
+ def check_consistency (
56
+ self , db_conn : Connection , table : str , id_column : str , positive : bool = True
57
+ ):
58
+ """Should be called during start up to test that the current value of
59
+ the sequence is greater than or equal to the maximum ID in the table.
60
+
61
+ This is to handle various cases where the sequence value can get out
62
+ of sync with the table, e.g. if Synapse gets rolled back to a previous
63
+ version and the rolled forwards again.
64
+ """
65
+ ...
66
+
31
67
32
68
class PostgresSequenceGenerator (SequenceGenerator ):
33
69
"""An implementation of SequenceGenerator which uses a postgres sequence"""
@@ -45,6 +81,50 @@ def get_next_mult_txn(self, txn: Cursor, n: int) -> List[int]:
45
81
)
46
82
return [i for (i ,) in txn ]
47
83
84
+ def check_consistency (
85
+ self , db_conn : Connection , table : str , id_column : str , positive : bool = True
86
+ ):
87
+ txn = db_conn .cursor ()
88
+
89
+ # First we get the current max ID from the table.
90
+ table_sql = "SELECT GREATEST(%(agg)s(%(id)s), 0) FROM %(table)s" % {
91
+ "id" : id_column ,
92
+ "table" : table ,
93
+ "agg" : "MAX" if positive else "-MIN" ,
94
+ }
95
+
96
+ txn .execute (table_sql )
97
+ row = txn .fetchone ()
98
+ if not row :
99
+ # Table is empty, so nothing to do.
100
+ txn .close ()
101
+ return
102
+
103
+ # Now we fetch the current value from the sequence and compare with the
104
+ # above.
105
+ max_stream_id = row [0 ]
106
+ txn .execute (
107
+ "SELECT last_value, is_called FROM %(seq)s" % {"seq" : self ._sequence_name }
108
+ )
109
+ last_value , is_called = txn .fetchone ()
110
+ txn .close ()
111
+
112
+ # If `is_called` is False then `last_value` is actually the value that
113
+ # will be generated next, so we decrement to get the true "last value".
114
+ if not is_called :
115
+ last_value -= 1
116
+
117
+ if max_stream_id > last_value :
118
+ logger .warning (
119
+ "Postgres sequence %s is behind table %s: %d < %d" ,
120
+ last_value ,
121
+ max_stream_id ,
122
+ )
123
+ raise IncorrectDatabaseSetup (
124
+ _INCONSISTENT_SEQUENCE_ERROR
125
+ % {"seq" : self ._sequence_name , "table" : table , "max_id_sql" : table_sql }
126
+ )
127
+
48
128
49
129
GetFirstCallbackType = Callable [[Cursor ], int ]
50
130
@@ -81,6 +161,12 @@ def get_next_id_txn(self, txn: Cursor) -> int:
81
161
self ._current_max_id += 1
82
162
return self ._current_max_id
83
163
164
+ def check_consistency (
165
+ self , db_conn : Connection , table : str , id_column : str , positive : bool = True
166
+ ):
167
+ # There is nothing to do for in memory sequences
168
+ pass
169
+
84
170
85
171
def build_sequence_generator (
86
172
database_engine : BaseDatabaseEngine ,
0 commit comments