You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: webdocs/design/high-availability/index.md
+56-16Lines changed: 56 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,12 +50,12 @@ sequenceDiagram
50
50
51
51
## Multiple mutex
52
52
53
-
We need something finer than (distributed) [mutex](#mutex): if a challenge A is under a CRUD operation, we don't need challenge B to not be able to handle another CRUD operation!
53
+
We need something _thinner_ than (distributed) [mutex](#mutex), scoped: if a challenge A is under a CRUD operation, we don't need challenge B to not be able to handle another CRUD operation!
54
54
55
-
We can imagine one (distributed) mutex per challenge such that they won't stuck one another.
56
-
Ok, that's fine... But what about instances?
55
+
To do so, we can imagine one (distributed) mutex per challenge such that they won't stuck one another by enabling parallel process-ability.
56
+
Ok, that's fine... But what about instances?
57
57
58
-
The same problem arise, the same solution: we can construct a chain of mutexes such that to perform a CRUD operation on an `Instance`, we lock the `Challenge` first, then the `Instance`, unlock the `Challenge`, execute the operation, and unlock the `Instance`. An API call from an upstream source or service is represented with this strategy as follows.
58
+
The same problem twice, the same solution twice: we can construct a chain of mutexes such that to perform a CRUD operation on an `Instance`, we lock the `Challenge` first, then the `Instance`, unlock the `Challenge`, execute the operation, and unlock the `Instance`. This enables re-scoping the critical code to a sub-model of the whole. An API call from an upstream source or service is represented with this strategy as follows.
59
59
60
60
```mermaid
61
61
sequenceDiagram
@@ -70,8 +70,8 @@ sequenceDiagram
70
70
API ->> Upstream: Response
71
71
```
72
72
73
-
One last thing, what if we want to query all challenges information (to build a dashboard, janitor outdated instances, ...) ?
74
-
We would need a "[Stop The World](https://en.wikipedia.org/wiki/Tracing_garbage_collection#Stop-the-world_vs._incremental_vs._concurrent)"-like mutex from which every challenge mutex would require context-relock before operation. To differenciate this from the Garbage Collector ideas, we call this a "Top-of-the-World" aka `TOTW`.
73
+
One last thing: what if we want to query all challenges information (to build a dashboard, janitor outdated instances, ...) ?
74
+
We would need a "[Stop The World](https://en.wikipedia.org/wiki/Tracing_garbage_collection#Stop-the-world_vs._incremental_vs._concurrent)"-like mutex from which every challenge mutex would require context-relock before operation. In order to avoid confusions with the Garbage Collector concept, we call this a "Top-of-the-World" aka `TOTW`.
75
75
76
76
The previous would now become the following.
77
77
@@ -95,27 +95,67 @@ Nevertheless, this availability is not high availability: we could enhance furth
All CRUD operations are not equal, and can be split in two groups:
99
-
- reading (Query, Read)
98
+
All CRUD operations are not equal, and can be simplistically separated in two groups:
99
+
- reading (Query, Read);
100
100
- writing (Create, Update, Delete).
101
101
102
-
The reading operations does not affect the state of an object, while writing ones does.
103
-
Moreover, in the case of chall-manager, reading operations are nearly instantaneous and writing ones are at least 10-seconds long.
102
+
The reading operations does not affect the state of an API object, while writing ones does.
103
+
Moreover, in the case of Chall-Manager, reading operations are nearly instantaneous and writing ones might at least be 10-seconds long.
104
104
How to deal with those unbalanced operations ?
105
105
106
-
As soon as discussions on [OS](https://en.wikipedia.org/wiki/Operating_system) began, researchers worked on the similar question and found solutions. They called this one the "reader-writer problem".
107
-
106
+
Looking back into history, as soon as discussions on [OS](https://en.wikipedia.org/wiki/Operating_system) began, researchers worked on the similar question and found solutions. They called this one the "reader-writer problem".
107
+
By previously modeling a Reader-Writer lock for faster API responses, and using the [Courtois et al. (1971)](https://doi.org/10.1145/362759.362813) second problem solution for writer-preference reader-writer solution, we would need 5 locks and 2 counters.
108
108
In our specific case, we want writer-preference as they would largely affect the state of the resources.
109
-
Using the [Courtois et al. (1971)](https://doi.org/10.1145/362759.362813) second problem solution for writer-preference reader-writer solution, we would need 5 locks and 2 counters.
110
109
111
-
For the technical implementation, we have multiple solutions: [etcd](https://etcd.io), [redis](https://redis.io/) or [valkey](https://valkey.io/).
112
-
We decided to choose etcd because it was already used by Kubernetes, and the [etcd client v3](https://github.com/etcdv3/etcd-client) already implement mutex and counters.
110
+
Note that this implementation does not guarantee fairness in request ordering, as they are not queued.
111
+
112
+
For the technical implementation, we have multiple solutions providing distributed mutexes and counters: [etcd](https://etcd.io), [redis](https://redis.io/) or [valkey](https://valkey.io/).
113
+
Our choice went to etcd for its ease of deployment with the Bitnami charts along with its proven efficiency (especially being the backend of Kubernetes), and thanks to the [etcd client v3](https://github.com/etcdv3/etcd-client) already implementing mutexes and counters.
113
114
114
115
{{< imgproc totw-identity-locks Fit "800x800" >}}
115
116
The triple chain of writer-preference reader-writer distributed locks.
116
117
{{< /imgproc >}}
117
118
118
-
With this approach, we could ensure data consistency throughout all replicas of chall-manager, and high-availability.
119
+
With this approach, we could ensure data consistency throughout all replicas of Chall-Manager, and high-availability through faster API responses.
120
+
121
+
## Recovery
122
+
123
+
When working with distributed systems, we have multiple factors that can affect the synchronization mecanisms: timings, networking, session management... And all can introduce errors.
124
+
These errors are not considered in Courtois et al. (1971) work. Nonetheless, locking a mutex can fail in the middle of a reader-writer lock or unlock operation.
125
+
If such error is not recovered, the operation is no longer (somewhat) atomic, thus some mutex states and counter values might have been altered incompletly (in a state that does no match Courtois et al. conditions).
126
+
127
+
Conceptualizing that RW operations should be atomic, we have to handle errors and implement the RW locks with recovery as a first principle.
128
+
In order to do such, we can model each reader lock, reader unlock, writer lock and writer unlock as a set of:
129
+
-**initial** steps, that prepare the operation;
130
+
-**altering** steps, that alter the state of the RW-preference, i.e., a mutex or counter that is not recovered to its initial state in its own lock/unlock execution, except for `w` due to writer-preference;
131
+
-**mandatory** steps, that must be performed once one altering step has run.
132
+
133
+
{{< imgproc recovery Fit "800x800" >}}
134
+
The steps decomposition of Courtois et al. (1971) Reader-Writer Writer-Preference lock and unlock operations.
135
+
{{< /imgproc >}}
136
+
137
+
In order to recover, the implementation must follow the algorithm:
138
+
- ahead of execution, for each initial step, virtually define a recovering counter-operation (e.g., if `P(x)` then its counter-operation is `V(x)`);
139
+
- create a stack of counter-operations;
140
+
- for each step during exeution, if is initial adds the counter-operation on a stack;
141
+
- for each step, if is a counter-operation in the stack, pop it out (considered recovered);
142
+
- once the first altering step is reached, we use a non-cancellable context such that we MUST terminate;
143
+
- if somewhere during execution there is an error, the stack counter-operations must be executed in reverse order of appending (LIFO ordering).
144
+
145
+
Using this algorithm, a cancelation in the request or a downstream error end up recovering on Courtois et al. (1971) conditions such that future execution are non-stuck.
146
+
Its recoverable operation principle is borrowed from the [sagas](https://doi.org/10.1145/38714.38742).
147
+
Nonetheless, if the underlying mutex and counter system fails during the altering steps, the states would end up stuck requiring a reset of the locks and counters.
148
+
149
+
Note that in the case of the writer unlock, we cannot easily recover from the `V(w)` in the initial steps as its counter-operation is a `P(w)`. Indeed, due to the unfaireness of the Courtois et al. (1971) second problem solution, we cannot prioritize this recovery over parallel requests. If we begin by executing it, and `P(mutex 2)` fails then we have to consider its initial steps as altering ones, increasing the potential for errors.
150
+
For this reason, we use a reasonable alternative that is more time-consuming but keep the properties of synchronization and preference.
151
+
152
+
```
153
+
P(mutex 2);
154
+
writercount := writecount - 1;
155
+
if writecount = 0 then V(r);
156
+
V(mutex 2);
157
+
V(w); # moved here, so MUST be executed once the critical steps are reached
0 commit comments