Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
## Picodata App Versioning
2021-12-15 Sergey V
### Disclaimer
Примеры кода это псевдокод. Naming требует отдельной проработки.
### Версионирование
Давайте для начала разберемся, что от чего зависит в контексте версионирования приложений Picodata.
Завимость сверху слева, зависимый снизу справа:
```
Schema version──┐
└─data? ────┤
├──rpc handlers─────┐
└──background jobs──┤
└──app version
```
У персистентного приложения всегда есть зависимость в виде схемы. Также может быть зависимость в виде данных (например, справочник категорий товаров на сайте интернет магазина).
Само приложение - это набор rpc хэндлеров и код, который выполняется в фоне.
По нашему соглашению схема в Picodata глобальная и eventually одинаковая на всех узлах.
* Если изменениями схемы считать императивные команды вроде ALTER TABLE, то версия схемы вследствие своей глобальности изменяется только вперед, от меньшей к большей (восстановление из бэкапов сейчас не рассматриваем)
* Код приложения на разных узлах локальный, может и будет отличаться при обновлениях и откатах. Версия кода меняется вперед и назад. Например, выкатили обновление с v1.0 на v1.1, потом решили откатить.
При откате приложения на старую версию схема остается новой версии.
Любая версия схемы может быть совместима или несовместима с любой версией приложения. **Совместимость** бывает номинальная и фактическая.
* **Номинальная** -- когда разработчик считает, что версии совместимы.
* **Фактическая** -- когда версии действительно совместимы.
### Был рассмотрен Костин вариант:
<https://gitlab.com/picodata/picodata/picodata/-/blob/f31698c7e805e5512e16138b6cf893970201aec9/docs/app-main-design.md#костина-версия>
#### Критика
Пример приложения:
```python
# this function was initially created in v1.0
fn magic_number() {
if random() > 0.1 {
return 42
}
##########################################
# these lines were added with version 2.0
if random() > 0.5 {
return 7
}
##########################################
return 0
}
fn main() {
version("1.0")
endpoint("magic_number", magic_number)
background_job("job1", some_job)
version("2.0")
endpoint("magic_number_plus1", fn() { return magic_number() + 1 })
}
```
* Код и endpoints прошлых версий могут измениться вследствие работы над кодом новой версии.
* После выкатки этого кода даже если вызов version(2.0) никогда не вернется (не соберет majority в кластере), endpoint версии 1.0 уже будет работать иначе. Это нежелательное поведение. Такого рода изменения могут быть зарыты глубоко в библиотечном коде и явно не прослеживаться.
* Другая проблема -- обновление кода существующих endpoints и background jobs. Если нужно изменить код эндпоинта `magic_number`, то как это сделать? Изменить внутри блока version 1.0? Или же переопределить endpoint ниже в блоке с новой версией? А в старом месте определения нужно удалить или закомментировать строчку? Если удалить - то мы как-бы изменили прошлую версию, и при запуске на чистом кластере поведение будет отличаться от поведения в существующем кластере с данными. Если оставить, то у нас в проекте появится dead code и копипаст.
* **В Костиной концепции у схемы и приложения одна версия**. Но что произойдет, если при очередном обновлении потребуется откатиться? Выкатили старую версию, но схема осталась новая. Приложение может быть фактически и даже номинально несовместимо с новой схемой, например, если запускаем приложение v2.0, а схема осталась от приложения v3.0. Приложение все равно попытается запуститься и может испортить данные. Даже если создать механизмы синхронизации, которые не дадут одновременно запустить разные версии приложения, проблема останется.
Для дальнейшей проработки этого варианта от Кости требуется больший уровень вовлеченности в процесс разработки концепции версионирования. Возможно, всё просто и круто, я протсто не понял. Открытые вопросы:
* https://gitlab.com/picodata/picodata/picodata/-/merge_requests/1#note_776679769
* https://gitlab.com/picodata/picodata/picodata/-/merge_requests/1#note_773605140
* https://gitlab.com/picodata/picodata/picodata/-/merge_requests/1#note_776723152
### Альтернативное предложение: отдельное версионирование приложений и схемы.
* Semver для схемы.
* Любой DDL выполняется в рамках инкремента версии схемы из любой версии приложения через глобальную синхронную кластерную операцию.
* В приложении разработчиком номинально задается совместимость с мажорной и минорной версией схемы. Версия схемы всегда растет. Если в приложении задано, что оно совместимо с версией схемы 2.2.0, то это приложение должно:
* содержать в себе код, который при запуске первым делом будет пытаться
идемпотентно обновить версию схемы в кластере до 2.2.0 и не начинать работу до момента, когда совместимая версия схемы применятся локально на инстансе.
* запускаться только на версиях схемы >=2.2.0,<3.0.0, иначе падать.
* Откатить версию схемы нельзя (кроме восстановления из бэкапа).
* Если текущая версия схемы в кластере больше, чем версия схемы, которую пытается выкатить приложение, происходит ошибка, и приложение падает.
* Если версия приложения номинально совместима с версией схемы, но фактически оказалась несовместима, то решить проблему нужно, выпустив и выкатив новую версию приложения, которое включает в себя fix кода и/или код обновления версии схемы в кластере для восстановления совместимости.
#### Пример ситуации, когда фактическая совместимость отличается от номинальной
Работало в кластере с версией схемы 1.1.1 приложение версии 2.2.2. При выкатке обновления приложения на v2.3.0, в котором номинально задана совместимость со схемой v1.1.1, выяснилось, что в приложении название колонки в таблице содержит опечатку. Для решения проблемы есть несколько выходов:
* Откатить приложение до версии 2.2.2.
* Выпустить и выкатить bugfix приложения v2.3.1, где исправить опечатку. Номинальную совместимость с версией схемы оставить v1.1.1.
* Выпустить и выкатить bugfix приложения v2.3.1, включающий обратнонесовместимое обновление схемы на v2.0.0 с переименованием колонки в таблице для соответствия имени колонки в коде приложения. В этой версии приложения номинально должна быть задана совместимость с версией схемы v2.0.0.
#### Пример приложения
```python
fn main(app) {
# added in app v5.0.0
app.cluster.update_schema("1.0.0",
[
sql("create table t ..."),
sql("create index i1 on t ..."),
# можно также добавить данные в справочники
sql("insert into t values ..."),
]
)
# added in app v5.1.0
app.cluster.update_schema("1.1.0",
# обратно совместимые изменения
[sql("alter table t add col42 text null")]
)
# added in app v5.2.0
app.cluster.update_schema("2.0.0",
# обратно несовместимые изменения схемы
[sql("rename table t to t2") ]
)
# fails with ERROR: cannot update schema version: current (2.0.0) >= new (1.99.0)
app.cluster.update_schema("1.99.0", [ sql("...") ] )
# Все еndpoints и background jobs всегда объявляются в каждой версии приложения
endpoint("ep1", ...)
endpoint("ep2", ...)
background_job(...)
}
```
#### Как должен работать вызов update_schema
При старте приложение первым делом накатывает в свой replicaset все примененные в кластере изменения схемы из глобального кластерного хранилища, это библиотечная функция из SDK.
Далее в пользовательском коде выполняется блокирующий вызов `update_schema(version: str, ddl_commands: array[Command])`, внутри которого:
1. Все версии схемы сохраняются в глобальном хранилище в кластере.
2. Список команд из нового изменения схемы записывается в raft log
3. Возврат только когда все команды выполнятся на majority
4. Список команд и обновление версии схемы в каждом инстансе выполняется транзакционно.
5. Обновление работает только если текущая версия схемы в кластере меньше предлагаемой в обновлении. Иначе ошибка.
6. Повторный вызов ранее успешно завершившегося вызова с той же версией это noop.
#### Проблемы
* drop index не работает в транзакции
* Приложения, где нужно динамически создавать и удалять spaces.
* От разработчиков требуется некоторая дисциплина: код изменений схемы нелья менять, можно только добавлять новый.
Изменение кода схемы под уже примененной в проде версией схемы может привести к неопределенному поведению. Но соблюдать
эту дисциплину относительно просто, потому что ее правила можно довольно кратко описать в документации. Кроме того,
разработчики, которые раньше работали с базами данных, уже привыкли к тому, что в проекте есть код миграций, который не
принято менять мосле релиза.
#### Ключевые особенности
* Приложения версионируются привычным разработчикам способом: захардкоженная константа содержит версию, и этой же версии соответствует tag в Git и релизные артефакты.
* Схема является частью кода приложения, но версионируется независимо от приложения
* Версионирование схемы глобальное в кластере, версионирование приложения локальное для инстанса
* Изменения схемы строго последовательные
* Приложение имеет номинальную зависимость от версии схемы в соответствии с правилами Semantic Versioning. Эта зависимость задается максимальным значением параметра `version` в последовательности вызовов метода `update_schema(version, commands)`.
* Все еndpoints и background jobs всегда объявляются в каждой версии приложения один раз
* Версия приложения зависит от API endpoints/rpc и background jobs, т.е. от функций кластера, которые видны пользователям API кластера и которые взаимодействуют с внешними системами. Сломали API -- нужно выпустить новую мажорную версию приложения. Сломали схему -- нужно присвоить коду обновления схемы новую мажорную версию.
* За изменения схемы ответственны и разработчики, т.к. они написали код изменений, зарелизили и выкатили приложение. Разработчики таким образом знают, какие изменения схемы под какими версиями долши до Production.
* Текущую версию схемы можно запросить в любой момент с любого узла кластера.
* Относительно легко можно понять, сможет ли определенная версия приложения выкатиться в существующий кластер. Для этого нужно сравнить максимальную версию схемы в приложении с текущей версией схемы в кластере.
#### Admin tools
Нужна команда, выводящая примерно такую таблицу
```
SUBJECT | SCHEMA_VERSION | APP_VERSION
----------------------------------------------------
cluster (majority) | 2.0.0 | -
instance1 | 2.0.0 | 5.1.0
instance2 | 1.2.3 (updating) | 4.3.2
instance3 | 2.0.0 | 5.1.0
```