- Merkezi sunucu tabanlı iş birlikli uygulamalarda metni dizi indeksleriyle düzenlemek, eşzamanlı düzenlemede konumların kaymasına yol açar; bu yüzden her karaktere küresel benzersiz ID verilip “belirli bir ID’nin arkasına ekle” yöntemiyle sunucu durumu güncellenir
- Gerçek servislerde kullanılan CRDT ve OT güçlüdür, ancak toplam sıralama algoritmaları veya operasyon dönüştürme kuralları karmaşık olduğundan, iç işleyişi uygulama gereksinimlerine göre değiştirmek zordur
- Önerilen yöntemde istemci ve sunucu
Array<{ id: ID; char?: string; isDeleted: boolean }> biçiminde bir ID listesi tutar; silinen karakterler de tombstone gibi bırakılarak sonraki ekleme konumu referanslarının bozulması engellenir
- İyimser yerel güncellemeler sunucu uzlaştırması (server reconciliation) ile ele alınır; uzak operasyon alındığında bekleyen yerel operasyonlar geri alınır, ardından uzak operasyon ve onaylanmamış yerel operasyonlar sırayla yeniden uygulanır
- Eşzamanlı ekleme sırası, zengin metin biçimlendirmesi, dağıtık varyantlar ve
Articulated kütüphanesi de ele alınır; sunucu, uygulamaya özgü anlama uygun şekilde ekleme-silmenin ötesinde esnek operasyonlar tanımlayabilir
İndeks tabanlı düzenlemenin eşzamanlı düzenlemede neden bozulduğu
- İş birlikli metin düzenlemede istemci, kullanıcının girdiği operasyonları sunucuya gönderir; sunucu da kendi otoritatif durumunu güncellemek zorundadır
- Metni bir karakter dizisi olarak görüp
index 17’ye " the" ekle gibi operasyonlar gönderildiğinde, sunucuya ulaşmadan önce başka bir kullanıcının eklemesi nedeniyle aynı indeks farklı bir konumu gösterebilir
- Örneğin Alice baş tarafa
" gray" eklerse Bob’un index 17 değeri artık eski konum değildir
- Sunucunun Bob’un operasyonunu
index 22 olarak rebase etmesi gerekir
- Temel mesele, istemcinin sunucuya hangi operasyonu göndereceği ve sunucunun bunu metni “açıkça doğru” bir şekilde güncellemek için nasıl yorumlaması gerektiğidir
- Bu indeks rebase sorunu yalnızca Google Docs gibi gerçek zamanlı iş birlikli uygulamalarda değil; liste öğesi ekleyen web formlarında veya satır içi yorumlar ve düzenleme geçmişiyle uğraşan tek iş parçacıklı yerel uygulamalarda da ortaya çıkabilir
CRDT ve OT’nin pratikte ağır gelen noktaları
- Mevcut çözümler kabaca CRDT ve OT olarak ikiye ayrılır
- CRDT, her karaktere değişmez bir ID veya “position” atar ve ID’leri özel bir ağaç dolaşımı gibi matematiksel bir toplam sıralama ile sıralar
- OT, eşzamanlı düzenlemeyi dikkate alarak operasyonun kendisini dönüştürür; örnekte
index 17 ekle operasyonunu index 22 ekleye çevirir
- İki yaklaşım da halihazırda gerçek servislerde kullanılıyor
- Google Docs OT kullanır
- Yjs CRDT kütüphanesi birçok uygulamada kullanılır
- Yük kavramsal karmaşıklıktan doğar
- Metin düzenleme CRDT’lerindeki toplam sıralama çoğu zaman akademik makalelerde tanımlanmış incelikli algoritmalardır
- OT algoritmaları cebirsel “dönüştürme özelliklerini” sağlamalıdır; durum sayısı karesel artar ve biçimsel doğrulama olmadan hatalara sık rastlanır
- Karmaşık algoritmalar uygulamayı da karmaşıklaştırır ve genellikle uzmanların yazdığı kütüphaneler bir ağ kara kutusu gibi kullanılır
- Kütüphanenin öngörmediği özellikler gerektiğinde monolitik yapı engel olur
- Büyük belgelerde yalnızca gerekli bölümleri belleğe alıp geri kalanını diskte tutmak
- Paragraf bazlı düzenleme yetkileri veya belirli biçimleri kullanma yetkisi gibi alt belge izinlerini sunucuda zorunlu kılmak
- Google Docs tarzı önerilen değişiklikleri metnin içinde veya yanında göstermek
- Metni Replicache gibi anahtar-değer depolarıyla senkronize etmesi kolay bir temsilde saklamak
- Ekleme-silme dışında metin taşıma, belge ağacını değiştirme, paragraf bölme-birleştirme gibi operasyonları desteklemek
Karakter ID’si ve “insert after” yöntemi
- Temel fikir, dizi indeksi yerine her karaktere küresel benzersiz ID vermektir
- Ana veri yapısı
Array<{ id: ID; char: string }> biçimindedir
- İstemci
index 17’ye ekle yerine sunucuya f1bdb70a arkasına " the" ekle gibi bir operasyon gönderir
- Sunucu hedef ID’yi bulur ve yeni karakterleri hemen arkasına koyar
- Yeni karakterlerin ID’lerini de istemci birlikte belirtmelidir
- Örnek:
f1bdb70a arkasına " the" metnini ids [...] ile ekle
- İstemci ID üretirse, sunucu yanıtı gelmeden sonraki
insert after operasyonlarında yeni ID’ye referans verebilir
- Silinen karakterler tamamen kaldırılırsa ekleme konumu kaybedilebilir
- Bob
26085702 arkasına ekleme yapmak isterken başka bir kullanıcı 26085702 karakterini silerse sunucu nereye ekleyeceğini bilemez
- Sunucu silinen ID’leri de iç listede tutmalıdır
- Düzeltilmiş durum temsili şöyledir
Array<{ id: ID; char?: string; isDeleted: boolean }>
- Kullanıcıya görünen metin, silinmemiş öğeler birleştirilerek oluşturulabilir
list.filter(elt => !elt.isDeleted).map(elt => elt.char).join('')
Ekleme ve silme işleme
- Karakter girildiğinde istemci ve sunucunun davranışı basittir
- İstemci ekleme noktasının hemen önündeki karakterin ID’si olan
before değerini bulur
- Yeni karakter için UUID gibi küresel benzersiz bir ID
id üretir
- Sunucuya
before arkasına char değerini id ile eklemesini söyleyen operasyonu gönderir
- Sunucu, silinen öğeler dahil
before değerini bulur ve o öğenin hemen arkasına { id, char, isDeleted: false } ekler
- Karakter silme de ID tabanlı işlenir
- İstemci silinecek karakterin
id değerini bulur
- Sunucuya ilgili ID’ye sahip öğeyi silmesini söyleyen operasyonu gönderir
- Sunucu ilgili öğeyi bulur ve henüz silinmemişse
entry.isDeleted = true olarak ayarlar
- Bu yöntem, CRDT veya OT makalelerini takip etmeden de sunucuya gönderilen düzenleme operasyonlarının konum problemini doğrudan çözer
- Basit dizi uygulaması her karakter için UUID saklamak zorunda olduğundan verimsiz olabilir; optimizasyon
Articulated içinde ele alınır
İyimser güncelleme ve sunucu uzlaştırması
- Google Docs tarzı iş birlikli düzenlemede kullanıcı, sunucu yanıtını beklemeden kendi girdisinin sonucunu hemen görmelidir
- Zor kısım, istemcide sunucu tarafından henüz onaylanmamış bekleyen yerel operasyonlar varken, sunucudan bunlarla eşzamanlı bir uzak operasyon almaktır
- Bu durumda CRDT şart değildir; sunucu uzlaştırması (server reconciliation) ile ele alınabilir
- Bekleyen tüm yerel operasyonları geri alarak istemci durumunu önceki sunucu durumu bakışına geri sarın
- Uzak operasyonu uygulayıp istemciyi sunucu durumuyla hizalayın
- Henüz onaylanmamış yerel operasyonları yeniden uygulayın
- Daha basit bir strateji olarak, bekleyen yerel operasyon varken uzak operasyon işlemeyi yasaklayan Wait for Ack vardır
- Bob’un istemcisi, kendi mesajının işlendiği sunucu durumunu alana kadar ilk sunucu mesajını yok sayabilir
- Bob yazmaya devam ederse veya ağ gecikmesi büyükse gecikme sonsuza kadar uzayabilir; bu yüzden sunucu uzlaştırmasına göre daha az gerçek zamanlıdır
CRDT’den ayrılan noktalar
- Önerilen yöntem, her karaktere ID vermesi ve
isDeleted işareti kullanması açısından CRDT ile bazı özellikleri paylaşır
- Fark, sıralamanın nasıl ele alındığındadır
- Bu yöntemde istemci sunucuya
X’i Y’nin arkasına ekle der; sunucu bunu aynen veya geliştiricinin tanımladığı başka bir şekilde işler
- Metin düzenleme CRDT’lerinde ID’ler karmaşık bir algoritmayla sıralanır
- Farklı metin düzenleme CRDT’leri arasındaki ayrımı yaratan ana nokta da bu ID sıralama algoritmasıdır; bu yaklaşım o kısmı atlar
Eşzamanlı eklemelerin yarattığı sonuç
- Birden fazla kullanıcı aynı konuma eşzamanlı yazarsa sonuç, sunucunun operasyonları aldığı sıranın tersi olarak yerleşir
- Örneğin metnin
"My name is" olduğunu ve Charlie’nin " Charlie", Dave’in " Dave" metnini eşzamanlı girdiğini varsayalım
- Charlie’nin operasyonu önce ulaşırsa sunucu
"My name is Charlie" oluşturur
- Dave’in operasyonu da aynı
is içindeki s ID’sinin arkasına eklendiğinden sonuç "My name is Dave Charlie" olur
- Aynı hedef ID’nin arkasına yönelik
insert after operasyonları, eşzamanlılık olmasa bile sunucunun alma sırasının tersine döner
- Yine de soldan sağa yazılan kelimeler karakter düzeyinde birbirine karışmaz
- Dave her karakteri ayrı operasyon olarak gönderse bile
a, D arkasına; v ise a arkasına eklenir
- Sunucu durumu
"My name is D Charlie" → "My name is Da Charlie" → "My name is Dav Charlie" → "My name is Dave Charlie" şeklinde değişir
- Sağdan sola yazma durumunda Charlie ve Dave’in operasyonları sunucuya çapraz sırada ulaşırsa sonuç metin de çaprazlanabilir
- Pratikte bu, iki kullanıcı aynı anda çevrimiçiyken birbirlerinin devam eden düzenlemesini görmezden geldiğinde ortaya çıkabilir
Sunucunun daha esnek operasyonlar tanımlayabilmesi
- Sunucu uzlaştırması kullanıldığında sunucu, istemci operasyonlarını fiilen istediği şekilde işleyebilir ve istemci sonunda aynı duruma ulaşır
- Bu, yalnızca sıkı cebirsel kuralları sağlayan operasyonlara izin veren CRDT ve OT ile karşıtlık oluşturur
- Aynı konumdaki eşzamanlı eklemeler için sunucu birkaç şekilde yanıt verebilir
- İlgili operasyonu yok sayıp no-op olarak işlemek
- ID’yi iç listeye eklemek ama hemen silinmiş olarak işaretlemek; böylece Dave’in sonraki operasyonları önceki ID’ye referans verebilir
- Metni eklemek ama iki kelimeye inceleme amaçlı özel biçim uygulamak
- Dave’in düzenlemesini metnin yanında gösterilen bir “öneri”ye dönüştürmek
- Metnin nasıl düzeltilmesi gerektiğini LLM’e sormak
- İstemci, kullanıcı niyetini daha iyi taşıyan operasyonlar da gönderebilir
insert before, paragrafın üstüne başlık oluştururken başlığın önceki paragraf sonundaki eşzamanlı eklemelerin ortasına girmesini önlemek için kullanılabilir
fix typo operasyonu ID X’e sahip color kelimesindeki o harfinin arkasına u ekle, ancak çevredeki kelime hâlâ color ise gibi koşullar taşıyabilir
- Sunucu, ekleme konumunun kendisinin sunucuya vardıktan sonra değiştiği operasyonlar da tanımlayabilir
- Aynı konumdaki eşzamanlı eklemeleri alfabetik sıraya göre yeniden sıralayabilir
- Sürükle-bırak için
move operasyonu eklenirse, taşınan metnin içindeki insert after, özgün konuma değil taşınan metnin içine uygulanabilir
Zengin metin biçimlendirmesini işleme
- Zengin metinde kalın, yazı boyutu ve hiperlink gibi satır içi biçimlendirmeler ele alınır
- Aralık biçimlendirmesi de indeks yerine karakter ID’leriyle ifade edilebilir
- Örnek:
ID X’ten ID Y’ye kadar bold uygula
ID X inclusive’ten ID Y exclusive’e kadar olarak tanımlanırsa aralık sonundaki eşzamanlı eklemeler de kalın yapılabilir
- ProseMirror gibi zengin metin editörleriyle birlikte kullanıldığında sunucu, ID X ve Y’nin güncel dizi indekslerini bulup yerel ProseMirror durumuna ilgili aralığı kalın yapmasını söyleyebilir
- ProseMirror daha sonra bu aralık içine eklenen metinde de kalınlığı koruyabilir
- Ancak sunucu
bold set to false gibi ekleme operasyonlarına göre farklı da işleyebilir
- İş birlikli zengin metnin semantiğini anlamak için Peritext essay kaynak olarak kullanılabilir
Dağıtık varyantlar ve CRDT ile bağlantısı
- Buraya kadar merkezi sunucunun operasyonların toplam sırasını sunucuya geliş sırası olarak belirlediği ve otoritatif durumu güncellediği varsayıldı
- Merkezi sunucunun olmadığı veya sunucunun opsiyonel olduğu uygulamalarda operasyonlara nihai toplam sıra dağıtık bir şekilde verilebilir
- Örneğin Lamport timestamps ile operasyonlar sıralanır
- Her istemci, o ana kadar aldığı operasyonları sırayla işlemenin sonucunu otoritatif durum kabul eder
- Bu durumda karakter bazlı ID’ler ve
insert after operasyonu dağıtık “sunucusuz” uzlaştırmada da çalışır
- Teknik olarak bu sonuç bir metin düzenleme CRDT’si olur
- Çünkü dağıtıktır ve nihai tutarlılığa sahip bir iş birlikli metin düzenleme algoritmasıdır
- Hangi sıralama yönteminin kullanıldığına bağlı olarak mevcut CRDT’lerle bağlantı kurulur
- Operasyonlar Lamport timestamp ile sıralanırsa sonuç liste sırası RGA / Causal Trees ile eşdeğerdir
- Lamport timestamp ile biçimlendirme operasyonları birlikte kullanılırsa davranış Peritext ile oldukça benzerdir
- Derinlik öncelikli topolojik sıralama kullanılırsa sonuç liste sırası Fugue ile eşdeğerdir
- Bu eşdeğerlik iddiasına dair ayrıntılı kanıt yazılmış değildir
Articulated: uygulamaya yardımcı kütüphane
- Gerçek uygulamada metnin kendisi ProseMirror durumu gibi başka bir yerde saklanabilir; bu yaklaşım için yalnızca aşağıdaki biçimde bir ID listesi gerekebilir
Array<{ id: ID; isDeleted: boolean }>
- Bu listede sık gereken dört işlem vardır
- ID ile güncel dizi indeksi arasında dönüşüm
- Belirtilen ID’nin arkasına yeni ID ekleme
- ID’yi silinmiş olarak işaretleme
- Saklama için durumu serileştirme ve geri yükleme
- Basit dizi bu işlemler için uygun değildir
- 1-3 numaralı işlemler doğrusal zaman alır
- Her karakter için nesne ve UUID saklandığından bellek ve depolama alanı büyüktür
- Articulated bu diziyle aynı işlevleri sağlayan küçük bir npm kütüphanesidir
- Ana veri yapısı
IdList, popüler metin düzenleme CRDT kütüphanelerine benzer optimizasyonlar kullanır
- ID
{ bunchId, counter } biçimindedir; bunchId birden çok ID’nin paylaşabileceği bir UUID’dir
- Soldan sağa ekleme yapılan tipik durumda olduğu gibi aynı bunch’taki ID’ler yan yana duruyorsa, bellek ve serileştirilmiş durumda tek bir nesne olarak saklanır
- Ana veri yapısı dizi değil B+Tree olduğu için metot çağrı süresi
log veya log^2 olur
IdList aynı zamanda kalıcı veri yapısıdır (persistent data structure)
- İstemci, sunucudan en son aldığı durumu ve iyimser durumu düşük maliyetle birlikte saklayabilir
- Uzak operasyon alındığında sunucunun son durumuna geri almak kolaydır
- Ek kaynaklar olarak docs, erken demos ve 300 SLOC’tan kısa basit uygulama IdListSimple sunulur
IdListSimple, optimizasyonları ve kalıcılığı atlar ama işlevsel olarak aynıdır ve fuzz tests ile doğrulanmıştır
1 yorum
Hacker News yorumları
Oldukça temiz. Algoritma, her metin karakterine UUID benzeri küresel olarak benzersiz bir ID vererek, sürekli değişen dizi indeksleri yerine zaman içinde tutarlı biçimde referans verilebilmesini sağlıyor.
İstemci, mevcut bir ID’ye referans veren “insert after” işlemini sunucuya gönderiyor; sunucu hedef ID’yi bulup yeni karakteri hemen arkasına ekliyor. Silme, yalnızca görünürde gizliyor; “insert after” konumunu hesaplayabilmek için karakterler saklanmaya devam ediyor. Metin düzenleme dışında da, örneğin oyun dünyası senkronizasyonu gibi alanlarda potansiyeli var gibi görünüyor.
ctrl+a,ctrl+x,ctrl+vyapınca bol şans dilemek gerekecek gibi.Böyle bir yazı görmek sevindirici. Birkaç yıl önce aynı yöntemi keşfetmiştim ve neden akademik literatürde pek görünmediğini merak etmiştim.
Ancak ben bunu merkeziyetsiz bağlamda CRDT olarak uyguladım; böylece değişme, idempotans ve birleşme gibi özellikler korunuyordu.
dict/map ya da keyfi tiplerden oluşan diziler gibi başka veri yapılarından bahsedilmemesi şaşırtıcıydı. Bunlara da kolayca genişletilebilse iyi olurdu. Deneyimlerime göre uygulamalarda saf ortak metin düzenlemeden çok işbirlikçi veri yapılarına daha sık ihtiyaç duyuluyor.
Senkron örnekler olan güncelleme doğrulama, kısmi yükleme ve üst düzey işlemler ilginç; ama Yjs gibi şeylerde bu özelliklerin olmamasının nedeni temel CRDT uygulaması mı, yoksa en baştan bu özellikleri yapmanın zor olması mı, bu argüman pek ikna edici görünmüyor.
OP’nin terminolojisiyle söylersem, yardımcı kütüphane kullanıcısının hatalı durumları engellemek ya da yönetmek için hafif bir anlam modeli mantığı araya sokabilmesi gerektiğini hep düşünmüşümdür. Örneğin bir yapılacak iş öğesi aynı anda hem
isDone: truehem destate: inProgressolamaz. Bağlantı verilen yazıdaki zengin metin biçimlendirme semantiğine benziyor.Tüm Git merge çakışmalarının otomatik olarak bir tarafı seçerek çözüldüğünü hayal edin. Çoğu zaman yanlış sonuç çıkar; bazen derlenmeyen kod bile oluşabilir. O anda bunu düzeltecek biri yoksa daha da karışık sonuçlara yol açar.
Bu yüzden CRDT’lerin daha yaygınlaşmadığını düşünüyorum. CRDT’ler yalnızca “var olduğunu düşündüğünüz problemi” çözer; asıl problem olan veriyi, geçerliliği ve anlamı koruyan çakışma çözümünü çözmez. Hatta çakışma çözüm yöntemini yalnızca deterministik olarak çoğaltılabilir biçimlerle sınırladığı için bu problemi daha da kötüleştirdiği söylenebilir.
CRDT’den farklı olan temel nokta şu gibi görünüyor: Merkezi bir sunucu varsa senkronizasyonu, yani eşzamanlı olaylar arasındaki sıranın kesinleştirilmesini veri yapısının kendisi leksikografik sırayla yapmasın, bunu sunucu yapsın.
Tüm iletişim yalnızca istemci-sunucu arasında gerçekleştiği ve istemciler arasında gerçekleşmediği için, bir istemci sunucuya bağlandığında sunucu, yeni uzak güncellemeleri göndermeden önce o istemcinin yerel işlemlerinin tamamını önce işlediğini garanti edebilir.
Bu yazının ana mesajı, CRDT/OT’nin tüm karmaşıklığının yalnızca merkezi sunucu yokken gerekli olduğu mu?
Diğer yorumların dediği gibi teknik olarak bu da bir CRDT ve oldukça genel bir biçim. Ayrıca işlemleri geri alıp yeniden oynatan uygulamanın kendisi de hiç kolay değil. Yine de her veri tipi için geleneksel CRDT/OT kullanmaktan daha basit olmasını umuyorum.
Bu alanın uzmanı değilim ama Automerge gibi CRDT’lerle temel fark sunucu koordinasyonu gibi görünüyor. Örneğin şu yazıya [1] bakarsanız, Automerge eşzamanlı eklemeleri işlerken sıra numarası kullanıyor; eklemeler aynı anda olursa üzerinde uzlaşılan agent ID sırasına dayanıyor. Buna karşılık bu yöntem, sunucunun geliş sırasına göre işlemesine dayanıyor.
Yazıda şöyle deniyor: “Metin düzenleme CRDT’lerinde havalı bir algoritma ID sırasını belirler. Bu sıralama algoritması, çeşitli metin düzenleme CRDT’leri arasındaki farkı oluşturur ve CRDT makalelerinin karmaşık kısmıdır. Biz bunu tamamen atlıyoruz.” Birçok uygulamada zaten merkezi sunucu olduğu için “havalı algoritma”dan kaçınılabileceği fikri makul geliyor. Ancak sunucu koordinasyonu, yerel düzenlemelerin geri alınmasını ve yeniden oynatılmasını gerektiriyor; bunun çok daha basit olduğundan %100 emin olmak zor. [1] https://josephg.com/blog/crdts-go-brrr/
Ancak Automerge pratikte bunu yapmıyor; metin işlemlerini geleneksel bir CRDT olan RGA ile ele alıyor. Muhtemelen işaret ettiğiniz gibi, işlem geri alma ve yeniden oynatma uygulaması kolay olmadığı içindir.
Yani bu optimize edilmemiş bir CRDT mi? Maksimum küme boyutunu 1 yapıp öylece zorlamak gibi?
Sunucu koordinasyonu kullanılması nedeniyle istemci tarafı koordinasyon zor olacak gibi. Sunucu güncellemeleri geldikçe uygulanırken akıcı bir editör UX’i nasıl korunur?
Örneğin istemcinin gönderdiği karakter ekleme isteği başarısız olursa yalnızca yeniden mi dener? Bu arada güncellemeler geldiyse? Düzenleme: “Client-Side” bölümünde bu durum kabul ediliyor; geri sarıp yeniden oynatma öneriliyor, daha basit bir seçenek olarak da bekleme kuyruğu boşalana kadar engelleme öneriliyor. Frontend açısından, belirtilmemiş UI/UX istisnaları uzayıp gidebilir; bu yüzden genel olarak CRDT daha basit bile olabilir gibi görünüyor. Ayrıca bağlantının sık koptuğu New York metrosunda düzenleme hissinin nasıl olacağını da merak ediyorum.
Gerçekten oldukça iyi çalışıyor. Ayrıntılar burada:
https://marijnhaverbeke.nl/blog/collaborative-editing.html
https://marijnhaverbeke.nl/blog/collaborative-editing-cm.htm...