Skip to content

Commit 77882b6

Browse files
authored
Document which versions of Synapse have compatible schema versions. (#16661)
1 parent b0ed14d commit 77882b6

File tree

6 files changed

+216
-1
lines changed

6 files changed

+216
-1
lines changed

.github/workflows/docs-pr.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,30 @@ on:
66
- docs/**
77
- book.toml
88
- .github/workflows/docs-pr.yaml
9+
- scripts-dev/schema_versions.py
910

1011
jobs:
1112
pages:
1213
name: GitHub Pages
1314
runs-on: ubuntu-latest
1415
steps:
1516
- uses: actions/checkout@v4
17+
with:
18+
# Fetch all history so that the schema_versions script works.
19+
fetch-depth: 0
1620

1721
- name: Setup mdbook
1822
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
1923
with:
2024
mdbook-version: '0.4.17'
2125

26+
- name: Setup python
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: "3.x"
30+
31+
- run: "pip install 'packaging>=20.0' 'GitPython>=3.1.20'"
32+
2233
- name: Build the documentation
2334
# mdbook will only create an index.html if we're including docs/README.md in SUMMARY.md.
2435
# However, we're using docs/README.md for other purposes and need to pick a new page

.github/workflows/docs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,22 @@ jobs:
5151
- pre
5252
steps:
5353
- uses: actions/checkout@v4
54+
with:
55+
# Fetch all history so that the schema_versions script works.
56+
fetch-depth: 0
5457

5558
- name: Setup mdbook
5659
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
5760
with:
5861
mdbook-version: '0.4.17'
5962

63+
- name: Setup python
64+
uses: actions/setup-python@v4
65+
with:
66+
python-version: "3.x"
67+
68+
- run: "pip install 'packaging>=20.0' 'GitPython>=3.1.20'"
69+
6070
- name: Build the documentation
6171
# mdbook will only create an index.html if we're including docs/README.md in SUMMARY.md.
6272
# However, we're using docs/README.md for other purposes and need to pick a new page

book.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ additional-css = [
3636
"docs/website_files/indent-section-headers.css",
3737
]
3838
additional-js = ["docs/website_files/table-of-contents.js"]
39-
theme = "docs/website_files/theme"
39+
theme = "docs/website_files/theme"
40+
41+
[preprocessor.schema_versions]
42+
command = "./scripts-dev/schema_versions.py"

changelog.d/16661.doc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add schema rollback information to documentation.

docs/upgrade.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ process, for example:
8888
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
8989
```
9090
91+
Generally Synapse database schemas are compatible across multiple versions, once
92+
a version of Synapse is deployed you may not be able to rollback automatically.
93+
The following table gives the version ranges and the earliest version they can
94+
be rolled back to. E.g. Synapse versions v1.58.0 through v1.61.1 can be rolled
95+
back safely to v1.57.0, but starting with v1.62.0 it is only safe to rollback to
96+
v1.61.0.
97+
98+
<!-- REPLACE_WITH_SCHEMA_VERSIONS -->
99+
91100
# Upgrading to v1.93.0
92101
93102
## Minimum supported Rust version

scripts-dev/schema_versions.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
# Copyright 2023 The Matrix.org Foundation C.I.C.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""A script to calculate which versions of Synapse have backwards-compatible
17+
database schemas. It creates a Markdown table of Synapse versions and the earliest
18+
compatible version.
19+
20+
It is compatible with the mdbook protocol for preprocessors (see
21+
https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#implementing-a-preprocessor-with-a-different-language):
22+
23+
Exit 0 to denote support for all renderers:
24+
25+
./scripts-dev/schema_versions.py supports <mdbook renderer>
26+
27+
Parse a JSON list from stdin and add the table to the proper documetnation page:
28+
29+
./scripts-dev/schema_versions.py
30+
31+
Additionally, the script supports dumping the table to stdout for debugging:
32+
33+
./scripts-dev/schema_versions.py dump
34+
"""
35+
36+
import io
37+
import json
38+
import sys
39+
from collections import defaultdict
40+
from typing import Any, Dict, Iterator, Optional, Tuple
41+
42+
import git
43+
from packaging import version
44+
45+
# The schema version has moved around over the years.
46+
SCHEMA_VERSION_FILES = (
47+
"synapse/storage/schema/__init__.py",
48+
"synapse/storage/prepare_database.py",
49+
"synapse/storage/__init__.py",
50+
"synapse/app/homeserver.py",
51+
)
52+
53+
54+
# Skip versions of Synapse < v1.0, they're old and essentially not
55+
# compatible with today's federation.
56+
OLDEST_SHOWN_VERSION = version.parse("v1.0")
57+
58+
59+
def get_schema_versions(tag: git.Tag) -> Tuple[Optional[int], Optional[int]]:
60+
"""Get the schema and schema compat versions for a tag."""
61+
schema_version = None
62+
schema_compat_version = None
63+
64+
for file in SCHEMA_VERSION_FILES:
65+
try:
66+
schema_file = tag.commit.tree / file
67+
except KeyError:
68+
continue
69+
70+
# We (usually) can't execute the code since it might have unknown imports.
71+
if file != "synapse/storage/schema/__init__.py":
72+
with io.BytesIO(schema_file.data_stream.read()) as f:
73+
for line in f.readlines():
74+
if line.startswith(b"SCHEMA_VERSION"):
75+
schema_version = int(line.split()[2])
76+
77+
# Bail early.
78+
break
79+
else:
80+
# SCHEMA_COMPAT_VERSION is sometimes across multiple lines, the easist
81+
# thing to do is exec the code. Luckily it has only ever existed in
82+
# a file which imports nothing else from Synapse.
83+
locals: Dict[str, Any] = {}
84+
exec(schema_file.data_stream.read().decode("utf-8"), {}, locals)
85+
schema_version = locals["SCHEMA_VERSION"]
86+
schema_compat_version = locals.get("SCHEMA_COMPAT_VERSION")
87+
88+
return schema_version, schema_compat_version
89+
90+
91+
def get_tags(repo: git.Repo) -> Iterator[git.Tag]:
92+
"""Return an iterator of tags sorted by version."""
93+
tags = []
94+
for tag in repo.tags:
95+
# All "real" Synapse tags are of the form vX.Y.Z.
96+
if not tag.name.startswith("v"):
97+
continue
98+
99+
# There's a weird tag from the initial react UI.
100+
if tag.name == "v0.1":
101+
continue
102+
103+
try:
104+
tag_version = version.parse(tag.name)
105+
except version.InvalidVersion:
106+
# Skip invalid versions.
107+
continue
108+
109+
# Skip pre- and post-release versions.
110+
if tag_version.is_prerelease or tag_version.is_postrelease or tag_version.local:
111+
continue
112+
113+
# Skip old versions.
114+
if tag_version < OLDEST_SHOWN_VERSION:
115+
continue
116+
117+
tags.append((tag_version, tag))
118+
119+
# Sort based on the version number (not lexically).
120+
return (tag for _, tag in sorted(tags, key=lambda t: t[0]))
121+
122+
123+
def calculate_version_chart() -> str:
124+
repo = git.Repo(path=".")
125+
126+
# Map of schema version -> Synapse versions which are at that schema version.
127+
schema_versions = defaultdict(list)
128+
# Map of schema version -> Synapse versions which are compatible with that
129+
# schema version.
130+
schema_compat_versions = defaultdict(list)
131+
132+
# Find ranges of versions which are compatible with a schema version.
133+
#
134+
# There are two modes of operation:
135+
#
136+
# 1. Pre-schema_compat_version (i.e. schema_compat_version of None), then
137+
# Synapse is compatible up/downgrading to a version with
138+
# schema_version >= its current version.
139+
#
140+
# 2. Post-schema_compat_version (i.e. schema_compat_version is *not* None),
141+
# then Synapse is compatible up/downgrading to a version with
142+
# schema version >= schema_compat_version.
143+
#
144+
# This is more generous and avoids versions that cannot be rolled back.
145+
#
146+
# See https://github.com/matrix-org/synapse/pull/9933 which was included in v1.37.0.
147+
for tag in get_tags(repo):
148+
schema_version, schema_compat_version = get_schema_versions(tag)
149+
150+
# If a schema compat version is given, prefer that over the schema version.
151+
schema_versions[schema_version].append(tag.name)
152+
schema_compat_versions[schema_compat_version or schema_version].append(tag.name)
153+
154+
# Generate a table which maps the latest Synapse version compatible with each
155+
# schema version.
156+
result = f"| {'Versions': ^19} | Compatible version |\n"
157+
result += f"|{'-' * (19 + 2)}|{'-' * (18 + 2)}|\n"
158+
for schema_version, synapse_versions in schema_compat_versions.items():
159+
result += f"| {synapse_versions[0] + ' – ' + synapse_versions[-1]: ^19} | {schema_versions[schema_version][0]: ^18} |\n"
160+
161+
return result
162+
163+
164+
if __name__ == "__main__":
165+
if len(sys.argv) == 3 and sys.argv[1] == "supports":
166+
# We don't care about the renderer which is being used, which is the second argument.
167+
sys.exit(0)
168+
elif len(sys.argv) == 2 and sys.argv[1] == "dump":
169+
print(calculate_version_chart())
170+
else:
171+
# Expect JSON data on stdin.
172+
context, book = json.load(sys.stdin)
173+
174+
for section in book["sections"]:
175+
if "Chapter" in section and section["Chapter"]["path"] == "upgrade.md":
176+
section["Chapter"]["content"] = section["Chapter"]["content"].replace(
177+
"<!-- REPLACE_WITH_SCHEMA_VERSIONS -->", calculate_version_chart()
178+
)
179+
180+
# Print the result back out to stdout.
181+
print(json.dumps(book))

0 commit comments

Comments
 (0)