07. Offline-First: SQLite WAL, Ring Buffer va Batching
gRPC & High-Performance System Design
07. Offline-First Caching: SQLite WAL, Ring Buffer va Batching
Har bir foydalanuvchi harakati (tugma bosilishi, ekran o'tishi, GPS koordinatasi, xatolar) haqida darhol serverga HTTP so'rov yuborish — bu mobil ilovaning o'limidir:
- Telefon antennasi har bir so'rovda uyqu rejimidan (Sleep state) to'liq quvvat rejimiga (Full Radio power) o'tadi va batareyani 2 soatda tugatadi.
- Foydalanuvchi metroga yoki liftga kirganda ma'lumotlar butunlay yo'qoladi.
Tizim dizaynining oltin qoidasi: "Local-First, Batch-Later" (Avval xotiraga yoz, keyin guruhlab yubor).
1. SQLite WAL (Write-Ahead Logging) Rejimi
Ko'pchilik dasturchilar SQLite qotib qolishidan (Database is Locked) shikoyat qilishadi. Sababi — standart rejimda yozish amali o'qish amalini to'liq bloklaydi.
Yechim — WAL Mode:
-- SQLite ochilganda majburiy pragmalar:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA temp_store = MEMORY;
PRAGMA cache_size = -64000; -- 64MB RAM kesh
WAL Rejimining Kuchli Tomoni:
- O'quvchilar (Readers) yozuvchilarni (Writers) hech qachon kutib turmaydi.
- Yozuvlar alohida
.wallog fayliga ketma-ket (Append-only) yoziladi. - Telegram va Apple tizim ilovalari barcha keshlarini aynan SQLite WAL rejimida yuritadi.
2. Xotiradagi Doiraviy Bufer (Ring Buffer)
Agar ilova offline holatda soatlab ishlasa, xotira to'lib ketmasligi (Out of Memory) uchun qat'iy o'lchamdagi Ring Buffer (Doiraviy Bufer) qo'llaniladi:
import Foundation
public final class RingBuffer<T> {
private var array: [T?]
private let capacity: Int
private var writeIndex = 0
private let lock = NSLock()
public init(capacity: Int = 1000) {
self.capacity = capacity
self.array = Array(repeating: nil, count: capacity)
}
public func append(_ element: T) {
lock.lock()
defer { lock.unlock() }
array[writeIndex % capacity] = element
writeIndex += 1
}
public func drainAll() -> [T] {
lock.lock()
defer { lock.unlock() }
let elements = array.compactMap { $0 }
self.array = Array(repeating: nil, count: capacity)
self.writeIndex = 0
return elements
}
}
3. Batch Sync Engine va gRPC Client-Streaming
Voqealar yig'ilgach, biz ularni 100 ta HTTP so'rov qilib emas, balki bitta gRPC Client-Streaming orqali serverga oqizamiz:
[Mobil Voqealar] ---> [Ring Buffer / SQLite]
|
(50 ta yig'ildi yoki 10 soniya o'tdi)
|
v
[gRPC Client-Stream] =================> [Go Server Ingest]
Paket 1: Event #1 |
Paket 2: Event #2 | (RAM ga batch yozish)
... v
Paket 50: Event #50 [PostgreSQL / ClickHouse]
Swift 6 Batch Sync Implementatsiyasi:
import Foundation
import Network
import GRPC
public actor BatchSyncEngine {
private let buffer = RingBuffer<Masterclass_V1_TelemetryEvent>(capacity: 500)
private let monitor = NWPathMonitor()
private var isConnected: Bool = true
public init() {
monitor.pathUpdateHandler = { [weak self] path in
Task {
await self?.updateNetworkStatus(path.status == .satisfied)
}
}
monitor.start(queue: DispatchQueue.global(qos: .background))
}
private func updateNetworkStatus(_ connected: Bool) {
self.isConnected = connected
if connected {
Task { await flushEvents() }
}
}
public func logEvent(eventName: String, payload: String) {
var event = Masterclass_V1_TelemetryEvent()
event.timestamp = Int64(Date().timeIntervalSince1970)
event.eventName = eventName
event.payload = payload
buffer.append(event)
}
public func flushEvents() async {
guard isConnected else { return }
let eventsToSend = buffer.drainAll()
guard !eventsToSend.isEmpty else { return }
// gRPC Client-Streaming orqali barcha voqealarni bitta ulanishda oqizamiz
print("🚀 \(eventsToSend.count) ta voqea serverga bitta paketda oqizilmoqda...")
}
}
Ushbu uslub orqali ilova hatto tog'da yoki samolyotda ham hech qanday xatoliksiz ishlaydi va internet paydo bo'lishi bilan barcha ma'lumotlarni batareyani tejagan holda serverga yuklaydi.