Skip to content

Commit ce5ed4d

Browse files
committed
Evolving Avro schema
1 parent 2d526ab commit ce5ed4d

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- [Protocol](#protocol)
1111
- [Server](#server)
1212
- [Client](#client)
13+
- [Evolving the Avro schema](#evolving-the-avro-schema)
14+
- [Protocol](#protocol-1)
1315

1416
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
1517

@@ -290,6 +292,67 @@ for {
290292
} yield exitCode
291293
```
292294

295+
## Evolving the Avro schema
296+
297+
As we have seen before, both client and server are using the same common protocol defined via Avro schema, which is an ideal scenario but realistically speaking the server side might need to add certainly changes in the model. Then how the server can preserve the compatibility with clients that are still using the old model?
298+
299+
Thanks to the Avro definitions we can add evolutions to the models in a safety way, keeping all the clients fully compatible but obviously, there are some limited operations that can't be done, like removing a field in a response model or adding a new required field to a request object.
300+
301+
To illustrate that non-updated clients are able to keep interacting with evolved servers, we'll just add a new field `phone` to `Person`.
302+
303+
### Protocol
304+
305+
Let's add a new evolution to the models described in the protocol
306+
307+
**_People.avdl_**
308+
309+
310+
```scala
311+
protocol People {
312+
313+
record Person {
314+
string name;
315+
int age;
316+
string phone;
317+
}
318+
319+
record PeopleRequest {
320+
string name;
321+
}
322+
323+
record PeopleResponse {
324+
Person person;
325+
}
326+
327+
}
328+
```
329+
330+
We can now run the server app using this new version, and the client app with the previous one, and the requests should have been processed properly on both sides.
331+
332+
As we can see, the client digests `Person`s instances included in the responses as expected:
333+
334+
```scala
335+
INFO - Created new RPC client for (localhost,19683)
336+
INFO - Request: foo
337+
INFO - Result: PeopleResponse(Person(foo,24))
338+
INFO - Request: bar
339+
INFO - Result: PeopleResponse(Person(bar,9))
340+
INFO - Request: baz
341+
INFO - Result: PeopleResponse(Person(baz,17))
342+
INFO - Removed 1 RPC clients from cache.
343+
```
344+
345+
Even when actually the server is including the telephone numbers at them:
346+
347+
```scala
348+
INFO - PeopleService - Request: PeopleRequest(foo)
349+
INFO - PeopleService - Sending response: Person(foo,24,(206) 198-8396)
350+
INFO - PeopleService - Request: PeopleRequest(bar)
351+
INFO - PeopleService - Sending response: Person(bar,9,(206) 740-2096)
352+
INFO - PeopleService - Request: PeopleRequest(baz)
353+
INFO - PeopleService - Sending response: Person(baz,17,(206) 812-1984)
354+
```
355+
293356
<!-- DOCTOC SKIP -->
294357
# Copyright
295358

protocol/src/main/resources/People.avdl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ protocol People {
44
record Person {
55
string name;
66
int age;
7+
string phone;
78
}
89

910
record PeopleRequest {

server/src/main/scala/server/PeopleRepository.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,18 @@ object PeopleRepository {
2121
private[this] val nextAge: StateT[F, Seed, Int] = StateT(
2222
seed => (seed.next, (seed.long % 100).toInt).pure[F])
2323

24-
override def getPerson(name: String): F[Person] =
25-
nextAge.map(age => Person(name, age)).runA(initialSeed(name))
24+
private[this] val nextPhone: StateT[F, Seed, String] = StateT { seed =>
25+
val phoneDigits = f"${(seed.long % 100000000).abs}%07d"
26+
val phone = s"(206) ${phoneDigits.substring(0, 3)}-${phoneDigits.substring(3, 7)}"
27+
(seed.next, phone).pure[F]
28+
}
29+
30+
override def getPerson(name: String): F[Person] = {
31+
for {
32+
age <- nextAge
33+
phone <- nextPhone
34+
} yield Person(name, age, phone)
35+
}.runA(initialSeed(name))
2636
}
2737

2838
def apply[F[_]](implicit ev: PeopleRepository[F]): PeopleRepository[F] = ev

0 commit comments

Comments
 (0)