1
0

synapse_port_db 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright 2015, 2016 OpenMarket Ltd
  4. # Copyright 2018 New Vector Ltd
  5. # Copyright 2019 The Matrix.org Foundation C.I.C.
  6. #
  7. # Licensed under the Apache License, Version 2.0 (the "License");
  8. # you may not use this file except in compliance with the License.
  9. # You may obtain a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. import argparse
  19. import curses
  20. import logging
  21. import sys
  22. import time
  23. import traceback
  24. from six import string_types
  25. import yaml
  26. from twisted.enterprise import adbapi
  27. from twisted.internet import defer, reactor
  28. from synapse.config.database import DatabaseConnectionConfig
  29. from synapse.config.homeserver import HomeServerConfig
  30. from synapse.logging.context import PreserveLoggingContext
  31. from synapse.storage._base import LoggingTransaction
  32. from synapse.storage.data_stores.main.client_ips import ClientIpBackgroundUpdateStore
  33. from synapse.storage.data_stores.main.deviceinbox import (
  34. DeviceInboxBackgroundUpdateStore,
  35. )
  36. from synapse.storage.data_stores.main.devices import DeviceBackgroundUpdateStore
  37. from synapse.storage.data_stores.main.events_bg_updates import (
  38. EventsBackgroundUpdatesStore,
  39. )
  40. from synapse.storage.data_stores.main.media_repository import (
  41. MediaRepositoryBackgroundUpdateStore,
  42. )
  43. from synapse.storage.data_stores.main.registration import (
  44. RegistrationBackgroundUpdateStore,
  45. )
  46. from synapse.storage.data_stores.main.room import RoomBackgroundUpdateStore
  47. from synapse.storage.data_stores.main.roommember import RoomMemberBackgroundUpdateStore
  48. from synapse.storage.data_stores.main.search import SearchBackgroundUpdateStore
  49. from synapse.storage.data_stores.main.state import MainStateBackgroundUpdateStore
  50. from synapse.storage.data_stores.main.stats import StatsStore
  51. from synapse.storage.data_stores.main.user_directory import (
  52. UserDirectoryBackgroundUpdateStore,
  53. )
  54. from synapse.storage.data_stores.state.bg_updates import StateBackgroundUpdateStore
  55. from synapse.storage.database import Database, make_conn
  56. from synapse.storage.engines import create_engine
  57. from synapse.storage.prepare_database import prepare_database
  58. from synapse.util import Clock
  59. logger = logging.getLogger("synapse_port_db")
  60. BOOLEAN_COLUMNS = {
  61. "events": ["processed", "outlier", "contains_url"],
  62. "rooms": ["is_public"],
  63. "event_edges": ["is_state"],
  64. "presence_list": ["accepted"],
  65. "presence_stream": ["currently_active"],
  66. "public_room_list_stream": ["visibility"],
  67. "devices": ["hidden"],
  68. "device_lists_outbound_pokes": ["sent"],
  69. "users_who_share_rooms": ["share_private"],
  70. "groups": ["is_public"],
  71. "group_rooms": ["is_public"],
  72. "group_users": ["is_public", "is_admin"],
  73. "group_summary_rooms": ["is_public"],
  74. "group_room_categories": ["is_public"],
  75. "group_summary_users": ["is_public"],
  76. "group_roles": ["is_public"],
  77. "local_group_membership": ["is_publicised", "is_admin"],
  78. "e2e_room_keys": ["is_verified"],
  79. "account_validity": ["email_sent"],
  80. "redactions": ["have_censored"],
  81. "room_stats_state": ["is_federatable"],
  82. }
  83. APPEND_ONLY_TABLES = [
  84. "event_reference_hashes",
  85. "events",
  86. "event_json",
  87. "state_events",
  88. "room_memberships",
  89. "topics",
  90. "room_names",
  91. "rooms",
  92. "local_media_repository",
  93. "local_media_repository_thumbnails",
  94. "remote_media_cache",
  95. "remote_media_cache_thumbnails",
  96. "redactions",
  97. "event_edges",
  98. "event_auth",
  99. "received_transactions",
  100. "sent_transactions",
  101. "transaction_id_to_pdu",
  102. "users",
  103. "state_groups",
  104. "state_groups_state",
  105. "event_to_state_groups",
  106. "rejections",
  107. "event_search",
  108. "presence_stream",
  109. "push_rules_stream",
  110. "ex_outlier_stream",
  111. "cache_invalidation_stream",
  112. "public_room_list_stream",
  113. "state_group_edges",
  114. "stream_ordering_to_exterm",
  115. ]
  116. end_error_exec_info = None
  117. class Store(
  118. ClientIpBackgroundUpdateStore,
  119. DeviceInboxBackgroundUpdateStore,
  120. DeviceBackgroundUpdateStore,
  121. EventsBackgroundUpdatesStore,
  122. MediaRepositoryBackgroundUpdateStore,
  123. RegistrationBackgroundUpdateStore,
  124. RoomBackgroundUpdateStore,
  125. RoomMemberBackgroundUpdateStore,
  126. SearchBackgroundUpdateStore,
  127. StateBackgroundUpdateStore,
  128. MainStateBackgroundUpdateStore,
  129. UserDirectoryBackgroundUpdateStore,
  130. StatsStore,
  131. ):
  132. def execute(self, f, *args, **kwargs):
  133. return self.db.runInteraction(f.__name__, f, *args, **kwargs)
  134. def execute_sql(self, sql, *args):
  135. def r(txn):
  136. txn.execute(sql, args)
  137. return txn.fetchall()
  138. return self.db.runInteraction("execute_sql", r)
  139. def insert_many_txn(self, txn, table, headers, rows):
  140. sql = "INSERT INTO %s (%s) VALUES (%s)" % (
  141. table,
  142. ", ".join(k for k in headers),
  143. ", ".join("%s" for _ in headers),
  144. )
  145. try:
  146. txn.executemany(sql, rows)
  147. except Exception:
  148. logger.exception("Failed to insert: %s", table)
  149. raise
  150. def set_room_is_public(self, room_id, is_public):
  151. raise Exception(
  152. "Attempt to set room_is_public during port_db: database not empty?"
  153. )
  154. class MockHomeserver:
  155. def __init__(self, config):
  156. self.clock = Clock(reactor)
  157. self.config = config
  158. self.hostname = config.server_name
  159. def get_clock(self):
  160. return self.clock
  161. def get_reactor(self):
  162. return reactor
  163. class Porter(object):
  164. def __init__(self, **kwargs):
  165. self.__dict__.update(kwargs)
  166. @defer.inlineCallbacks
  167. def setup_table(self, table):
  168. if table in APPEND_ONLY_TABLES:
  169. # It's safe to just carry on inserting.
  170. row = yield self.postgres_store.db.simple_select_one(
  171. table="port_from_sqlite3",
  172. keyvalues={"table_name": table},
  173. retcols=("forward_rowid", "backward_rowid"),
  174. allow_none=True,
  175. )
  176. total_to_port = None
  177. if row is None:
  178. if table == "sent_transactions":
  179. (
  180. forward_chunk,
  181. already_ported,
  182. total_to_port,
  183. ) = yield self._setup_sent_transactions()
  184. backward_chunk = 0
  185. else:
  186. yield self.postgres_store.db.simple_insert(
  187. table="port_from_sqlite3",
  188. values={
  189. "table_name": table,
  190. "forward_rowid": 1,
  191. "backward_rowid": 0,
  192. },
  193. )
  194. forward_chunk = 1
  195. backward_chunk = 0
  196. already_ported = 0
  197. else:
  198. forward_chunk = row["forward_rowid"]
  199. backward_chunk = row["backward_rowid"]
  200. if total_to_port is None:
  201. already_ported, total_to_port = yield self._get_total_count_to_port(
  202. table, forward_chunk, backward_chunk
  203. )
  204. else:
  205. def delete_all(txn):
  206. txn.execute(
  207. "DELETE FROM port_from_sqlite3 WHERE table_name = %s", (table,)
  208. )
  209. txn.execute("TRUNCATE %s CASCADE" % (table,))
  210. yield self.postgres_store.execute(delete_all)
  211. yield self.postgres_store.db.simple_insert(
  212. table="port_from_sqlite3",
  213. values={"table_name": table, "forward_rowid": 1, "backward_rowid": 0},
  214. )
  215. forward_chunk = 1
  216. backward_chunk = 0
  217. already_ported, total_to_port = yield self._get_total_count_to_port(
  218. table, forward_chunk, backward_chunk
  219. )
  220. defer.returnValue(
  221. (table, already_ported, total_to_port, forward_chunk, backward_chunk)
  222. )
  223. @defer.inlineCallbacks
  224. def handle_table(
  225. self, table, postgres_size, table_size, forward_chunk, backward_chunk
  226. ):
  227. logger.info(
  228. "Table %s: %i/%i (rows %i-%i) already ported",
  229. table,
  230. postgres_size,
  231. table_size,
  232. backward_chunk + 1,
  233. forward_chunk - 1,
  234. )
  235. if not table_size:
  236. return
  237. self.progress.add_table(table, postgres_size, table_size)
  238. if table == "event_search":
  239. yield self.handle_search_table(
  240. postgres_size, table_size, forward_chunk, backward_chunk
  241. )
  242. return
  243. if table in (
  244. "user_directory",
  245. "user_directory_search",
  246. "users_who_share_rooms",
  247. "users_in_pubic_room",
  248. ):
  249. # We don't port these tables, as they're a faff and we can regenreate
  250. # them anyway.
  251. self.progress.update(table, table_size) # Mark table as done
  252. return
  253. if table == "user_directory_stream_pos":
  254. # We need to make sure there is a single row, `(X, null), as that is
  255. # what synapse expects to be there.
  256. yield self.postgres_store.db.simple_insert(
  257. table=table, values={"stream_id": None}
  258. )
  259. self.progress.update(table, table_size) # Mark table as done
  260. return
  261. forward_select = (
  262. "SELECT rowid, * FROM %s WHERE rowid >= ? ORDER BY rowid LIMIT ?" % (table,)
  263. )
  264. backward_select = (
  265. "SELECT rowid, * FROM %s WHERE rowid <= ? ORDER BY rowid LIMIT ?" % (table,)
  266. )
  267. do_forward = [True]
  268. do_backward = [True]
  269. while True:
  270. def r(txn):
  271. forward_rows = []
  272. backward_rows = []
  273. if do_forward[0]:
  274. txn.execute(forward_select, (forward_chunk, self.batch_size))
  275. forward_rows = txn.fetchall()
  276. if not forward_rows:
  277. do_forward[0] = False
  278. if do_backward[0]:
  279. txn.execute(backward_select, (backward_chunk, self.batch_size))
  280. backward_rows = txn.fetchall()
  281. if not backward_rows:
  282. do_backward[0] = False
  283. if forward_rows or backward_rows:
  284. headers = [column[0] for column in txn.description]
  285. else:
  286. headers = None
  287. return headers, forward_rows, backward_rows
  288. headers, frows, brows = yield self.sqlite_store.db.runInteraction(
  289. "select", r
  290. )
  291. if frows or brows:
  292. if frows:
  293. forward_chunk = max(row[0] for row in frows) + 1
  294. if brows:
  295. backward_chunk = min(row[0] for row in brows) - 1
  296. rows = frows + brows
  297. rows = self._convert_rows(table, headers, rows)
  298. def insert(txn):
  299. self.postgres_store.insert_many_txn(txn, table, headers[1:], rows)
  300. self.postgres_store.db.simple_update_one_txn(
  301. txn,
  302. table="port_from_sqlite3",
  303. keyvalues={"table_name": table},
  304. updatevalues={
  305. "forward_rowid": forward_chunk,
  306. "backward_rowid": backward_chunk,
  307. },
  308. )
  309. yield self.postgres_store.execute(insert)
  310. postgres_size += len(rows)
  311. self.progress.update(table, postgres_size)
  312. else:
  313. return
  314. @defer.inlineCallbacks
  315. def handle_search_table(
  316. self, postgres_size, table_size, forward_chunk, backward_chunk
  317. ):
  318. select = (
  319. "SELECT es.rowid, es.*, e.origin_server_ts, e.stream_ordering"
  320. " FROM event_search as es"
  321. " INNER JOIN events AS e USING (event_id, room_id)"
  322. " WHERE es.rowid >= ?"
  323. " ORDER BY es.rowid LIMIT ?"
  324. )
  325. while True:
  326. def r(txn):
  327. txn.execute(select, (forward_chunk, self.batch_size))
  328. rows = txn.fetchall()
  329. headers = [column[0] for column in txn.description]
  330. return headers, rows
  331. headers, rows = yield self.sqlite_store.db.runInteraction("select", r)
  332. if rows:
  333. forward_chunk = rows[-1][0] + 1
  334. # We have to treat event_search differently since it has a
  335. # different structure in the two different databases.
  336. def insert(txn):
  337. sql = (
  338. "INSERT INTO event_search (event_id, room_id, key,"
  339. " sender, vector, origin_server_ts, stream_ordering)"
  340. " VALUES (?,?,?,?,to_tsvector('english', ?),?,?)"
  341. )
  342. rows_dict = []
  343. for row in rows:
  344. d = dict(zip(headers, row))
  345. if "\0" in d["value"]:
  346. logger.warning("dropping search row %s", d)
  347. else:
  348. rows_dict.append(d)
  349. txn.executemany(
  350. sql,
  351. [
  352. (
  353. row["event_id"],
  354. row["room_id"],
  355. row["key"],
  356. row["sender"],
  357. row["value"],
  358. row["origin_server_ts"],
  359. row["stream_ordering"],
  360. )
  361. for row in rows_dict
  362. ],
  363. )
  364. self.postgres_store.db.simple_update_one_txn(
  365. txn,
  366. table="port_from_sqlite3",
  367. keyvalues={"table_name": "event_search"},
  368. updatevalues={
  369. "forward_rowid": forward_chunk,
  370. "backward_rowid": backward_chunk,
  371. },
  372. )
  373. yield self.postgres_store.execute(insert)
  374. postgres_size += len(rows)
  375. self.progress.update("event_search", postgres_size)
  376. else:
  377. return
  378. def build_db_store(
  379. self, db_config: DatabaseConnectionConfig, allow_outdated_version: bool = False,
  380. ):
  381. """Builds and returns a database store using the provided configuration.
  382. Args:
  383. db_config: The database configuration
  384. allow_outdated_version: True to suppress errors about the database server
  385. version being too old to run a complete synapse
  386. Returns:
  387. The built Store object.
  388. """
  389. self.progress.set_state("Preparing %s" % db_config.config["name"])
  390. engine = create_engine(db_config.config)
  391. hs = MockHomeserver(self.hs_config)
  392. with make_conn(db_config, engine) as db_conn:
  393. engine.check_database(
  394. db_conn, allow_outdated_version=allow_outdated_version
  395. )
  396. prepare_database(db_conn, engine, config=self.hs_config)
  397. store = Store(Database(hs, db_config, engine), db_conn, hs)
  398. db_conn.commit()
  399. return store
  400. @defer.inlineCallbacks
  401. def run_background_updates_on_postgres(self):
  402. # Manually apply all background updates on the PostgreSQL database.
  403. postgres_ready = (
  404. yield self.postgres_store.db.updates.has_completed_background_updates()
  405. )
  406. if not postgres_ready:
  407. # Only say that we're running background updates when there are background
  408. # updates to run.
  409. self.progress.set_state("Running background updates on PostgreSQL")
  410. while not postgres_ready:
  411. yield self.postgres_store.db.updates.do_next_background_update(100)
  412. postgres_ready = yield (
  413. self.postgres_store.db.updates.has_completed_background_updates()
  414. )
  415. @defer.inlineCallbacks
  416. def run(self):
  417. try:
  418. # we allow people to port away from outdated versions of sqlite.
  419. self.sqlite_store = self.build_db_store(
  420. DatabaseConnectionConfig("master-sqlite", self.sqlite_config),
  421. allow_outdated_version=True,
  422. )
  423. # Check if all background updates are done, abort if not.
  424. updates_complete = (
  425. yield self.sqlite_store.db.updates.has_completed_background_updates()
  426. )
  427. if not updates_complete:
  428. sys.stderr.write(
  429. "Pending background updates exist in the SQLite3 database."
  430. " Please start Synapse again and wait until every update has finished"
  431. " before running this script.\n"
  432. )
  433. defer.returnValue(None)
  434. self.postgres_store = self.build_db_store(
  435. self.hs_config.get_single_database()
  436. )
  437. yield self.run_background_updates_on_postgres()
  438. self.progress.set_state("Creating port tables")
  439. def create_port_table(txn):
  440. txn.execute(
  441. "CREATE TABLE IF NOT EXISTS port_from_sqlite3 ("
  442. " table_name varchar(100) NOT NULL UNIQUE,"
  443. " forward_rowid bigint NOT NULL,"
  444. " backward_rowid bigint NOT NULL"
  445. ")"
  446. )
  447. # The old port script created a table with just a "rowid" column.
  448. # We want people to be able to rerun this script from an old port
  449. # so that they can pick up any missing events that were not
  450. # ported across.
  451. def alter_table(txn):
  452. txn.execute(
  453. "ALTER TABLE IF EXISTS port_from_sqlite3"
  454. " RENAME rowid TO forward_rowid"
  455. )
  456. txn.execute(
  457. "ALTER TABLE IF EXISTS port_from_sqlite3"
  458. " ADD backward_rowid bigint NOT NULL DEFAULT 0"
  459. )
  460. try:
  461. yield self.postgres_store.db.runInteraction("alter_table", alter_table)
  462. except Exception:
  463. # On Error Resume Next
  464. pass
  465. yield self.postgres_store.db.runInteraction(
  466. "create_port_table", create_port_table
  467. )
  468. # Step 2. Get tables.
  469. self.progress.set_state("Fetching tables")
  470. sqlite_tables = yield self.sqlite_store.db.simple_select_onecol(
  471. table="sqlite_master", keyvalues={"type": "table"}, retcol="name"
  472. )
  473. postgres_tables = yield self.postgres_store.db.simple_select_onecol(
  474. table="information_schema.tables",
  475. keyvalues={},
  476. retcol="distinct table_name",
  477. )
  478. tables = set(sqlite_tables) & set(postgres_tables)
  479. logger.info("Found %d tables", len(tables))
  480. # Step 3. Figure out what still needs copying
  481. self.progress.set_state("Checking on port progress")
  482. setup_res = yield defer.gatherResults(
  483. [
  484. self.setup_table(table)
  485. for table in tables
  486. if table not in ["schema_version", "applied_schema_deltas"]
  487. and not table.startswith("sqlite_")
  488. ],
  489. consumeErrors=True,
  490. )
  491. # Step 4. Do the copying.
  492. self.progress.set_state("Copying to postgres")
  493. yield defer.gatherResults(
  494. [self.handle_table(*res) for res in setup_res], consumeErrors=True
  495. )
  496. # Step 5. Do final post-processing
  497. yield self._setup_state_group_id_seq()
  498. self.progress.done()
  499. except Exception:
  500. global end_error_exec_info
  501. end_error_exec_info = sys.exc_info()
  502. logger.exception("")
  503. finally:
  504. reactor.stop()
  505. def _convert_rows(self, table, headers, rows):
  506. bool_col_names = BOOLEAN_COLUMNS.get(table, [])
  507. bool_cols = [i for i, h in enumerate(headers) if h in bool_col_names]
  508. class BadValueException(Exception):
  509. pass
  510. def conv(j, col):
  511. if j in bool_cols:
  512. return bool(col)
  513. if isinstance(col, bytes):
  514. return bytearray(col)
  515. elif isinstance(col, string_types) and "\0" in col:
  516. logger.warning(
  517. "DROPPING ROW: NUL value in table %s col %s: %r",
  518. table,
  519. headers[j],
  520. col,
  521. )
  522. raise BadValueException()
  523. return col
  524. outrows = []
  525. for i, row in enumerate(rows):
  526. try:
  527. outrows.append(
  528. tuple(conv(j, col) for j, col in enumerate(row) if j > 0)
  529. )
  530. except BadValueException:
  531. pass
  532. return outrows
  533. @defer.inlineCallbacks
  534. def _setup_sent_transactions(self):
  535. # Only save things from the last day
  536. yesterday = int(time.time() * 1000) - 86400000
  537. # And save the max transaction id from each destination
  538. select = (
  539. "SELECT rowid, * FROM sent_transactions WHERE rowid IN ("
  540. "SELECT max(rowid) FROM sent_transactions"
  541. " GROUP BY destination"
  542. ")"
  543. )
  544. def r(txn):
  545. txn.execute(select)
  546. rows = txn.fetchall()
  547. headers = [column[0] for column in txn.description]
  548. ts_ind = headers.index("ts")
  549. return headers, [r for r in rows if r[ts_ind] < yesterday]
  550. headers, rows = yield self.sqlite_store.db.runInteraction("select", r)
  551. rows = self._convert_rows("sent_transactions", headers, rows)
  552. inserted_rows = len(rows)
  553. if inserted_rows:
  554. max_inserted_rowid = max(r[0] for r in rows)
  555. def insert(txn):
  556. self.postgres_store.insert_many_txn(
  557. txn, "sent_transactions", headers[1:], rows
  558. )
  559. yield self.postgres_store.execute(insert)
  560. else:
  561. max_inserted_rowid = 0
  562. def get_start_id(txn):
  563. txn.execute(
  564. "SELECT rowid FROM sent_transactions WHERE ts >= ?"
  565. " ORDER BY rowid ASC LIMIT 1",
  566. (yesterday,),
  567. )
  568. rows = txn.fetchall()
  569. if rows:
  570. return rows[0][0]
  571. else:
  572. return 1
  573. next_chunk = yield self.sqlite_store.execute(get_start_id)
  574. next_chunk = max(max_inserted_rowid + 1, next_chunk)
  575. yield self.postgres_store.db.simple_insert(
  576. table="port_from_sqlite3",
  577. values={
  578. "table_name": "sent_transactions",
  579. "forward_rowid": next_chunk,
  580. "backward_rowid": 0,
  581. },
  582. )
  583. def get_sent_table_size(txn):
  584. txn.execute(
  585. "SELECT count(*) FROM sent_transactions" " WHERE ts >= ?", (yesterday,)
  586. )
  587. (size,) = txn.fetchone()
  588. return int(size)
  589. remaining_count = yield self.sqlite_store.execute(get_sent_table_size)
  590. total_count = remaining_count + inserted_rows
  591. defer.returnValue((next_chunk, inserted_rows, total_count))
  592. @defer.inlineCallbacks
  593. def _get_remaining_count_to_port(self, table, forward_chunk, backward_chunk):
  594. frows = yield self.sqlite_store.execute_sql(
  595. "SELECT count(*) FROM %s WHERE rowid >= ?" % (table,), forward_chunk
  596. )
  597. brows = yield self.sqlite_store.execute_sql(
  598. "SELECT count(*) FROM %s WHERE rowid <= ?" % (table,), backward_chunk
  599. )
  600. defer.returnValue(frows[0][0] + brows[0][0])
  601. @defer.inlineCallbacks
  602. def _get_already_ported_count(self, table):
  603. rows = yield self.postgres_store.execute_sql(
  604. "SELECT count(*) FROM %s" % (table,)
  605. )
  606. defer.returnValue(rows[0][0])
  607. @defer.inlineCallbacks
  608. def _get_total_count_to_port(self, table, forward_chunk, backward_chunk):
  609. remaining, done = yield defer.gatherResults(
  610. [
  611. self._get_remaining_count_to_port(table, forward_chunk, backward_chunk),
  612. self._get_already_ported_count(table),
  613. ],
  614. consumeErrors=True,
  615. )
  616. remaining = int(remaining) if remaining else 0
  617. done = int(done) if done else 0
  618. defer.returnValue((done, remaining + done))
  619. def _setup_state_group_id_seq(self):
  620. def r(txn):
  621. txn.execute("SELECT MAX(id) FROM state_groups")
  622. curr_id = txn.fetchone()[0]
  623. if not curr_id:
  624. return
  625. next_id = curr_id + 1
  626. txn.execute("ALTER SEQUENCE state_group_id_seq RESTART WITH %s", (next_id,))
  627. return self.postgres_store.db.runInteraction("setup_state_group_id_seq", r)
  628. ##############################################
  629. # The following is simply UI stuff
  630. ##############################################
  631. class Progress(object):
  632. """Used to report progress of the port
  633. """
  634. def __init__(self):
  635. self.tables = {}
  636. self.start_time = int(time.time())
  637. def add_table(self, table, cur, size):
  638. self.tables[table] = {
  639. "start": cur,
  640. "num_done": cur,
  641. "total": size,
  642. "perc": int(cur * 100 / size),
  643. }
  644. def update(self, table, num_done):
  645. data = self.tables[table]
  646. data["num_done"] = num_done
  647. data["perc"] = int(num_done * 100 / data["total"])
  648. def done(self):
  649. pass
  650. class CursesProgress(Progress):
  651. """Reports progress to a curses window
  652. """
  653. def __init__(self, stdscr):
  654. self.stdscr = stdscr
  655. curses.use_default_colors()
  656. curses.curs_set(0)
  657. curses.init_pair(1, curses.COLOR_RED, -1)
  658. curses.init_pair(2, curses.COLOR_GREEN, -1)
  659. self.last_update = 0
  660. self.finished = False
  661. self.total_processed = 0
  662. self.total_remaining = 0
  663. super(CursesProgress, self).__init__()
  664. def update(self, table, num_done):
  665. super(CursesProgress, self).update(table, num_done)
  666. self.total_processed = 0
  667. self.total_remaining = 0
  668. for table, data in self.tables.items():
  669. self.total_processed += data["num_done"] - data["start"]
  670. self.total_remaining += data["total"] - data["num_done"]
  671. self.render()
  672. def render(self, force=False):
  673. now = time.time()
  674. if not force and now - self.last_update < 0.2:
  675. # reactor.callLater(1, self.render)
  676. return
  677. self.stdscr.clear()
  678. rows, cols = self.stdscr.getmaxyx()
  679. duration = int(now) - int(self.start_time)
  680. minutes, seconds = divmod(duration, 60)
  681. duration_str = "%02dm %02ds" % (minutes, seconds)
  682. if self.finished:
  683. status = "Time spent: %s (Done!)" % (duration_str,)
  684. else:
  685. if self.total_processed > 0:
  686. left = float(self.total_remaining) / self.total_processed
  687. est_remaining = (int(now) - self.start_time) * left
  688. est_remaining_str = "%02dm %02ds remaining" % divmod(est_remaining, 60)
  689. else:
  690. est_remaining_str = "Unknown"
  691. status = "Time spent: %s (est. remaining: %s)" % (
  692. duration_str,
  693. est_remaining_str,
  694. )
  695. self.stdscr.addstr(0, 0, status, curses.A_BOLD)
  696. max_len = max([len(t) for t in self.tables.keys()])
  697. left_margin = 5
  698. middle_space = 1
  699. items = self.tables.items()
  700. items = sorted(items, key=lambda i: (i[1]["perc"], i[0]))
  701. for i, (table, data) in enumerate(items):
  702. if i + 2 >= rows:
  703. break
  704. perc = data["perc"]
  705. color = curses.color_pair(2) if perc == 100 else curses.color_pair(1)
  706. self.stdscr.addstr(
  707. i + 2, left_margin + max_len - len(table), table, curses.A_BOLD | color
  708. )
  709. size = 20
  710. progress = "[%s%s]" % (
  711. "#" * int(perc * size / 100),
  712. " " * (size - int(perc * size / 100)),
  713. )
  714. self.stdscr.addstr(
  715. i + 2,
  716. left_margin + max_len + middle_space,
  717. "%s %3d%% (%d/%d)" % (progress, perc, data["num_done"], data["total"]),
  718. )
  719. if self.finished:
  720. self.stdscr.addstr(rows - 1, 0, "Press any key to exit...")
  721. self.stdscr.refresh()
  722. self.last_update = time.time()
  723. def done(self):
  724. self.finished = True
  725. self.render(True)
  726. self.stdscr.getch()
  727. def set_state(self, state):
  728. self.stdscr.clear()
  729. self.stdscr.addstr(0, 0, state + "...", curses.A_BOLD)
  730. self.stdscr.refresh()
  731. class TerminalProgress(Progress):
  732. """Just prints progress to the terminal
  733. """
  734. def update(self, table, num_done):
  735. super(TerminalProgress, self).update(table, num_done)
  736. data = self.tables[table]
  737. print(
  738. "%s: %d%% (%d/%d)" % (table, data["perc"], data["num_done"], data["total"])
  739. )
  740. def set_state(self, state):
  741. print(state + "...")
  742. ##############################################
  743. ##############################################
  744. if __name__ == "__main__":
  745. parser = argparse.ArgumentParser(
  746. description="A script to port an existing synapse SQLite database to"
  747. " a new PostgreSQL database."
  748. )
  749. parser.add_argument("-v", action="store_true")
  750. parser.add_argument(
  751. "--sqlite-database",
  752. required=True,
  753. help="The snapshot of the SQLite database file. This must not be"
  754. " currently used by a running synapse server",
  755. )
  756. parser.add_argument(
  757. "--postgres-config",
  758. type=argparse.FileType("r"),
  759. required=True,
  760. help="The database config file for the PostgreSQL database",
  761. )
  762. parser.add_argument(
  763. "--curses", action="store_true", help="display a curses based progress UI"
  764. )
  765. parser.add_argument(
  766. "--batch-size",
  767. type=int,
  768. default=1000,
  769. help="The number of rows to select from the SQLite table each"
  770. " iteration [default=1000]",
  771. )
  772. args = parser.parse_args()
  773. logging_config = {
  774. "level": logging.DEBUG if args.v else logging.INFO,
  775. "format": "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s",
  776. }
  777. if args.curses:
  778. logging_config["filename"] = "port-synapse.log"
  779. logging.basicConfig(**logging_config)
  780. sqlite_config = {
  781. "name": "sqlite3",
  782. "args": {
  783. "database": args.sqlite_database,
  784. "cp_min": 1,
  785. "cp_max": 1,
  786. "check_same_thread": False,
  787. },
  788. }
  789. hs_config = yaml.safe_load(args.postgres_config)
  790. if "database" not in hs_config:
  791. sys.stderr.write("The configuration file must have a 'database' section.\n")
  792. sys.exit(4)
  793. postgres_config = hs_config["database"]
  794. if "name" not in postgres_config:
  795. sys.stderr.write("Malformed database config: no 'name'\n")
  796. sys.exit(2)
  797. if postgres_config["name"] != "psycopg2":
  798. sys.stderr.write("Database must use the 'psycopg2' connector.\n")
  799. sys.exit(3)
  800. config = HomeServerConfig()
  801. config.parse_config_dict(hs_config, "", "")
  802. def start(stdscr=None):
  803. if stdscr:
  804. progress = CursesProgress(stdscr)
  805. else:
  806. progress = TerminalProgress()
  807. porter = Porter(
  808. sqlite_config=sqlite_config,
  809. progress=progress,
  810. batch_size=args.batch_size,
  811. hs_config=config,
  812. )
  813. reactor.callWhenRunning(porter.run)
  814. reactor.run()
  815. if args.curses:
  816. curses.wrapper(start)
  817. else:
  818. start()
  819. if end_error_exec_info:
  820. exc_type, exc_value, exc_traceback = end_error_exec_info
  821. traceback.print_exception(exc_type, exc_value, exc_traceback)
  822. sys.exit(5)