Синхронізація даних в додатках реального часу c Theron

Іноді я малюю собі граф того, як повинна виглядати архітектура сучасних систем і знаходжу ті моменти процесу розробки, які можуть бути поліпшені і ті практики, які можуть бути застосовані для поліпшення цих процесів. Після чергової такої ітерації я ще раз переконався, що існують приголомшливі фреймворки та методології для розробки і серверної та клієнтської частин, але синхронізація даних між клієнтом, сервером і базою даних працює не так, як того вимагають сучасні реалії: швидке реагування на зміну стану системи, розгалуженість та асинхронність обробки даних, повторне використання раніше оброблених даних.

В останні роки вимоги до сучасних програм і методи їх розробки значно змінилися. Більшість таких додатків використовують асинхронну модель, що складається з безлічі слабо пов'язаних компонентів (микросервисов). Користувачі ж хочуть, щоб додаток працювало безвідмовно і завжди було в актуальному стані (дані повинні бути синхронізовані в будь-який момент часу), простіше кажучи, користувачі відчувають себе більш комфортно, коли їм не потрібно кожен раз натискати кнопку «Оновити» або повністю перезавантажувати додаток, якщо щось пішло не так. Під катом трохи теорії і практики і повноцінне додатком c відкритим вихідним кодом зі стеком розробки React, Redux/Saga, Node, TypeScript і нашим проектом Theron.

image
Rick and Morty. Рік відкриває безліч порталів.

Я використовував різні сервіси для синхронізації і зберігання даних в реальному часі, більшість з яких згадано в цієї статті. Але кожен раз, як тільки додаток розвивалося в більш складний продукт, ставало очевидним, що система занадто сильно залежить від одного провайдера послуг і в ній немає тієї необхідної гнучкості, яку дає створення своєї мікроархітектури з безліччю диверсифікованих сервісів-сателітів, використання класичних баз даних (SQL та NoSQL) і написання коду, натомість конструкторам і панелей управління BaaS. Такий підхід, дійсно, більш складний на початковій стадії розробки прототипу, але він окупає себе в майбутньому.

Результатом моїх досліджень стала Theron. Theron — сервіс для створення сучасних додатків реального часу. Реактивне сховище даних Theron безперервно транслює зміни, що відбулися в базі даних, виходячи з запиту до неї. Трохи більше ніж за чотири місяці невеликою командою з двох розробників ми реалізували базову архітектуру, основні критерії якої:

  • Швидке створення нових додатків і безболісна міграція існуючих на Theron.
  • Використання сучасних практик при створенні асинхронних, розподілених і відмовостійких систем і ізоморфізм компонентів системи.
  • Розподілена інтеграція на низькому рівні з базами даних, такими як Postgres і Mongo.
  • Легка інтеграція з сучасними фреймворками, такими як React, Angular і їх друзями: ReactiveX, Redux т. д.
  • Сфокусованість на вирішення завдання синхронізації даних, а не надання повного стека розробки і подальшого "вендор локинга".
  • Основна логіка додатків (у тому числі аутентифікація і права доступу) повинна реалізовуватися розробниками на їх стороні.
Реактивні канали
Мені сподобався функціональний підхід ще тоді, коли я познайомився з одним із найстаріших функціональних мов програмування, орієнтованих на символьні обчислення Рефал. Пізніше, сам того не усвідомлюючи, я почав використовувати реактивну парадигму програмування, і з часом велика частина моєї роботи будувалася на цих підходах.

Theron побудований на основі ReactiveX. Фундаментальний концепт в Theron —реактивні канали, які надають гнучкий спосіб трансляції даних різних сегментів користувачів. Theron використовує класичний Pub/Sub шаблон проектування. Для створення нового каналу (кількість необмежено) і стрімінг даних достатньо лише створити нову підписку.

Після установки (англ.), імпортуйте Theron і створити нового клієнта:

import { Theron } from 'theron';

const theron = new Theron('https://therondb.com', { app: 'YOUR_APP_NAME' });

Створення нового клієнта не встановлює нового WebSocket підключення і не починає синхронізацію даних. З'єднання встановлюється тільки тоді, коли створюється підписка, за умови того, що немає іншого активного підключення. Тобто в рамках реактивного програмування клієнт Theron і канали — це "cold вами" об'єкти.

Створіть нову підписку:

const channel = theron.join('the-world');

const subscription = channel.subscribe(
message => {
console.log(message.payload);
},

err => {
console.log(err);
},

() => {
console.log('done');
}
);

Коли канал більше не потрібен — відпишіть:

subscription.unsubscribe();

Відправка даних клієнтам, підписаних на цей канал, з боку сервера (Node.js також проста:

import { Theron } from 'theron';

const theron = new Theron('https://therondb.com', { app: 'YOUR_APP_NAME', secret: 'YOUR_SECRET_KEY' });

theron.publish('the-world', { message: 'Greatings from Cybertron!' }).subscribe(
res => {
console.log(res);
},

err => {
console.log(err);
},

() => {
console.log('done');
},
);

Theron використовує експоненціальний бэкофф (включений за умовчанням) при втраті з'єднання або при виникненні некритичних помилок (англ.): помилки, коли можлива повторна підписка на канал.

Реалізація багатьох алгоритмів у рамках реактивного програмування витончена і проста, наприклад, експоненціальний бэкофф клієнтської бібліотеці Theron виглядає приблизно так:

let attemp = 0; 

const rescueChannel = channel.retryWhen(errs =>
errs.switchMap(() => Вами.timer(Math.pow(2, attemp + 1) * 500).do(() => attemp++))
).do(() => attemp = 0);

Інтеграція з базою даних
Як було сказано вище, Theron — це реактивне сховище даних: система повідомлень про зміни, яка безперервно транслює оновлення по захищених каналах для вашого додатки, виходячи із звичайного запиту SQL до бази даних. Theron аналізує запит до бази даних і відправляє артефакти даних, з допомогою яких можна відтворити вихідні дані.

Theron інтегрований на даний момент з Postgres; інтеграція з Mongo в процесі розробки.
Розглянемо, як це працює на прикладі життєвого циклу простого списку, що складається з перших трьох елементів, впорядкованого в алфавітному порядку:

image

Перед тим, як ми продовжимо, підключіть базу даних до Theron, ввівши дані для доступу до неї в панелі управління:

image

Внутрішнє пристрій захоплення (locking) бази даних — велика тема для окремої статті в майбутньому. Theron — розподілена система, тому пулу підключень до бази даних обмежений 10-ю (з можливістю збільшення до 20 загальними зв'язками.

1. Створення нової передплати

Theron працює з SQL запитами, тому ваш сервер повинен повертати не результат виконання запиту, а вихідний SQL запит. Наприклад, у нашому випадку JSON відповідь сервера може виглядати так:

{ "query": "SELECT * FROM todos ORDER BY name LIMIT 3" }

На стороні клієнта почнемо трансляцію даних для нашого SQL запиту, створивши нову підписку:

import { Theron } from 'theron';

const theron = new Theron('https://therondb.com', { app: 'YOUR_APP_NAME' });

const subscription = theron.watch('/todos').subscribe(
action => {
console.log(action); // Інструкції Theron'а
},

err => {
console.log(err);
},

() => {
console.log('complete');
}
);

Theron відправить GET запит '/todos' вашого сервера, перевірить валідність повернутого SQL запиту і почне трансляцію початкових інструкцій з необхідними даними, якщо цей запит не був раніше скэширован на стороні клієнта.

Інструкція TheronRowArtefact — це звичайний JavaScript об'єкт із самими даними `payload` і типом інструкції `type`. Основні типи інструкцій:

  • ROW_ADDED — доданий новий елемент.
  • ROW_REMOVED — елемент був видалений.
  • ROW_MOVED — елемент був змінений.
  • ROW_CHANGED — елемент був змінений.
  • BEGIN_TRANSACTION — новий блок синхронізації.
  • COMMIT_TRANSACTION — синхронізацію завершено успішно.
  • ROLLBACK_TRANSACTION — при синхронізації виникла помилка.
Припустимо, що в базі даних вже існує кілька елементів A, B, C. Тоді зміна стану клієнта можна представити наступним чином (зліва — було, праворуч — стало):

Id Name Id Name
1 A
2 B
3 C
Інструкції Theron для даного стану:

  1. { type: BEGIN_TRANSACTION }
  2. { type: <font color="#449d44">ROW_ADDED</font>, payload: { row: { id: 1, name: 'A' }, prevRowId: null } }
  3. { type: <font color="#449d44">ROW_ADDED</font>, payload: { row: { id: 2, name: 'B' }, prevRowId: 1 } 
  4. { type: <font color="#449d44">ROW_ADDED</font>, payload: { row: { id: 3, name: 'C' }, prevRowId: 2 } }
  5. { type: BEGIN_TRANSACTION }
Кожен блок синхронізації починаються та закінчуються інструкціями BEGIN_TRANSACTION і COMMIT_TRANSACTION. Для коректної сортування елементів на стороні клієнта Theron додатково надсилає дані про попередньому елементі.

2. Користувач перейменовує елемент A (1) D (1)

Припустимо, що користувач перейменовує елемент A (1) D (1). Так як SQL запит впорядковує елементи в алфавітному порядку, то відбудеться сортування елементів, і стан клієнта зміниться наступним чином:
Id Name Id Name
1 A 2 B
2 B 3 C
3 C 1 D
Інструкції Theron для даного стану:

  1. { type: BEGIN_TRANSACTION }
  2. { type: <font color="#31b0d5">ROW_CHANGED</font>, payload: { row: { id: 1, name: 'D' }, prevRowId: 3 } }
  3. { type: <font color="#ec971f">ROW_MOVED</font>, payload: { row: { id: 1, name: 'D' }, prevRowId: 3 } }
  4. { type: <font color="#ec971f">ROW_MOVED</font>, payload: { row: { id: 2, name: 'B' }, prevRowId: null } }
  5. { type: <font color="#ec971f">ROW_MOVED</font>, payload: { row: { id: 3, name: 'C' }, prevRowId: 2 } }
  6. { type: COMMIT_TRANSACTION }
3. Користувач створює новий елемент A (4)

Припустимо, що користувач створює новий елемент A (4). Так як наш SQL запит обмежує дані першими трьома елементами, то на стороні клієнта відбудеться видалення елемента D (1) і стан клієнта зміниться наступним чином:
Id Name Id Name
2 B 4 A
3 C 2 B
1 D 3 C
1 D
Інструкції Theron для даного стану:

  • { type: BEGIN_TRANSACTION }
  • { type: <font color="#449d44">ROW_ADDED</font>, payload: { row: { id: 4, name: 'A' }, prevRowId: null } }
  • { type: <font color="#ec971f">ROW_MOVED</font>, payload: { row: { id: 2, name: 'B' }, prevRowId: 4 } }
  • { type: <font color="#ec971f">ROW_MOVED</font>, payload: { row: { id: 3, name: 'C' }, prevRowId: 2 } }
  • { type: <font color="#c9302c">ROW_REMOVED</font>, payload: { row: { id: 1, name: 'D' }, prevRowId: 3 } }
  • { type: COMMIT_TRANSACTION }
4. Користувач видаляє елемент D (1)

Припустимо, що користувач видаляє елемент D (1) з бази даних. Theron в цьому випадку не відправить нових інструкцій, так як це зміна в базі даних не впливає на дані, які повертаються нашим SQL запиту, і відповідно не впливає на стан клієнта:
Id Name Id Name
4 A 4 A
2 B 2 B
3 C 3 C
Обробка інструкцій на стороні клієнта

Тепер, знаючи як Theron працює з даними, ми можемо реалізувати логіку відтворення даних на стороні клієнта. Алгоритм досить простий: ми будемо використовувати тип інструкції та метадані попереднього елемента для коректного позиціонування елементів у масиві. В реальному додатку потрібно використовувати, наприклад, бібліотеку Immutable.js для роботи з масивами та оператор scanпример.

import { ROW_ADDED, ROW_CHANGED, ROW_MOVED, ROW_REMOVED } from 'theron';

let todos = [];

const subscription = theron.watch('/todos').subscribe(
action => {
switch (action.type) {
case ROW_ADDED:
const index = nextIndexForRow(rows, action.prevRowId)
if (index !== -1) {
rows.splice(index, 0, action.row);
}
break;

case ROW_CHANGED:
const index = indexForRow(rows, action.row.id);
if (index !== -1) {
rows[index] = action.row;
}
break;

case ROW_MOVED:
const index = indexForRow(rows, action.row.id);
if (index !== -1) {
const row = list.splice(curPos, 1)[0];
const newIndex = nextIndexForRow(rows, action.prevRowId);
rows.splice(newIndex, 0, row);
}
break;

case ROW_REMOVED:
const index = indexForRow(rows, action.row.id);
if (index !== -1) {
list.splice(index, 1);
}
break;
}
},

err => {
console.log(err);
}
);

function indexForRow(rows, rowId) {
return rows.findIndex(row => row.id === rowId);
}

function nextIndexForRow(rows, prevRowId) {
if (prevRowId === null) {
return 0;
}

const index = indexForRow(rows, prevRowId);

if (index === -1) {
return rows.length;
} else {
return index + 1;
}
}

Час прикладів
Вивчати іноді краще, грунтуючись на готових прикладах: тому ось обіцяне додаток, опубліковане під ліцензією MIT — https://github.com/therondb/figure. Figure — це сервіс для роботи з HTML-формами в статичних сайтах; стек розробки — React, Redux/Saga, Node, TypeScript і, звичайно, Theron. Наприклад, ми використовуємо Figure для формування листа передплатників нашого блогу і сайту документації (https://github.com/therondb/therondb.com):

image

Висновок
Крім виправлення гіпотетичної тонни помилок і класичного написання клієнтських бібліотек під популярні платформи, ми працюємо над виділенням в незалежний компонент зворотного проксі-сервера і балансувальника. Ідея полягає в тому, щоб можна було створювати на стороні сервера API, до якого можна звертатися як через звичайні HTTP запити, так і через постійне підключення WebSocket. В наступній статті про архітектуру Theron я напишу про це більш докладно.

Команда у нас невелика, але енергійна, і ми любимо експериментувати. Theron знаходиться в активній розробці: є безліч ідей і моментів, які потрібно реалізувати і поліпшити. З задоволенням вислухаємо будь-яку критику, приймемо поради і конструктивно це обговоримо.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.