<style> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Gowun+Dodum&display=swap" rel="stylesheet"> body { font-family:'Gowun Dodum', sans-serif; } </style> ### 0. 문제가 성립할 수 있는 가? - 벡터를 이용한다 -> 실행된 op 버전을 op의 actor로 나누어 관리하겠다. - **change를 보낼 땐 자신의 최신 벡터를 보낸다**. - peer간 change를 주고 받으며 상대의 상태를 벡터를 통해 알 수 있다. - GC 룰 - **다른 모든 노드들이 해당 이벤트를 받은 경우에만 GC를 진행한다**. - 이는 다른 노드들의 change의 vector clock을 통해 확인 - **GC는 현존하는 peer들의 모든 벡터를 가지고 있는 상태에서 진행되어야 한다**. - 현존하는 peer의 기준은 무엇인가? - attach되어 있고, activated된 client - attach 함수는 presense를 put하기에 다른 peer들은 attach된 client의 벡터를 가짐을 보장 ( snap shot 예외 ) - attach 룰 - **attach시 presense change에 자신의 초기 벡터를 서버에 푸시** - detach - **자신이 나간다는 표시를 change에 남겨야한다**. - **이후 자신의 벡터를 제외한 나머지 벡터 제거하면 안된다.** ( GC가 돌아버려 다시 attach시 문제) ### 1. [이슈](https://github.com/yorkie-team/yorkie/issues/723) 구체화 ![image](https://hackmd.io/_uploads/H1LnSDPq6.png) ![image](https://hackmd.io/_uploads/rJJ5fwwcT.png) ### 2. 실제 구현에서의 vector clock - update 및 sync 과정 ![image](https://hackmd.io/_uploads/rkBxf8ksT.png) - document 생성과 attach ![image](https://hackmd.io/_uploads/rJklPyRca.png) - detach - 어떻게 다른 클라이언트의 detach 정보를 알려줄 것인지? ![image](https://hackmd.io/_uploads/By7GJgR56.png) - 이슈의 테스트 코드의 도식화 ```go= // gc_test.go t.Run("should not garbage collection", func(t *testing.T) { ctx := context.Background() d1 := document.New(helper.TestDocKey(t)) err := c1.Attach(ctx, d1) assert.NoError(t, err) d2 := document.New(helper.TestDocKey(t)) err = c2.Attach(ctx, d2) assert.NoError(t, err) err = d1.Update(func(root *json.Object, p *presence.Presence) error { root.SetNewText("text").Edit(0, 0, "a").Edit(1, 1, "b").Edit(2, 2, "c") return nil }, "sets text") assert.NoError(t, err) err = c1.Sync(ctx) assert.NoError(t, err) err = c2.Sync(ctx) assert.NoError(t, err) err = d2.Update(func(root *json.Object, p *presence.Presence) error { root.GetText("text").Edit(2, 2, "c") return nil }, "insert c") assert.NoError(t, err) err = d1.Update(func(root *json.Object, p *presence.Presence) error { root.GetText("text").Edit(1, 3, "") return nil }, "delete bd") assert.NoError(t, err) assert.Equal(t, 2, d1.GarbageLen()) assert.Equal(t, 0, d2.GarbageLen()) err = c2.Sync(ctx) assert.NoError(t, err) err = c2.Sync(ctx) assert.NoError(t, err) err = c1.Sync(ctx) assert.NoError(t, err) err = c1.Sync(ctx) assert.NoError(t, err) assert.Equal(t, 2, d1.GarbageLen()) assert.Equal(t, 0, d2.GarbageLen()) err = d2.Update(func(root *json.Object, p *presence.Presence) error { root.GetText("text").Edit(2, 2, "1") return nil }, "insert 1") assert.NoError(t, err) err = c2.Sync(ctx) // GC purge 4@A node assert.NoError(t, err) err = c2.Sync(ctx) assert.NoError(t, err) assert.Equal(t, 2, d1.GarbageLen()) assert.Equal(t, 0, d2.GarbageLen()) err = c1.Sync(ctx) assert.NoError(t, err) assert.Equal(t, 2, d1.GarbageLen()) assert.Equal(t, 0, d2.GarbageLen()) }) ``` ![image](https://hackmd.io/_uploads/Bk2sM0dqa.png) ### 3. Snap shot 관련 서버의 Latest synced vector 컨셉 - 주황색 <- 서버의 누적된 벡터 (synced된 vector들 중 가장 최근의 벡터이다.) - 오른쪽 아래 client c가 attach시, change와 snap shot을 교환하고, 서버의 Latest synced vector를 가져와 vector clock을 업데이트. ![image](https://hackmd.io/_uploads/ByCBQFw5a.png) #### 3-1. 실제 구현 ![image](https://hackmd.io/_uploads/SJ6xd0-ja.png) - 서버는 snap shot으로부터 document를 빌드할 때 synced vector map 또한 같이 만든다. - 이후 pull 시 snapshot을 받는 상황이라면, snap shot으로 부터 만들어진 synced vector map을 pack에 담아 보내준다. > snapshot으로부터 pull할 때 - pullSnapshot( ) - BuildDocumentForServerSeq( ) - NewInternalDocumentFromSnapshot( ) -> snapshot을 이용해 doc 만든다. - 기존 snapshot의 synced vector map을 가져온다. - snapshot과 서버 sequence 사이 change를 apply하며 synced vector map을 업데이트한다. - request의 change를 apply - request를 한 client의 vector clock이 document에 만들어진다. - initActorID와 request Actor ID가 synced vector map에 공존하는 상태 - doc.syncedVectorMap을 반환 > storeSnapshot / snapshot저장 - 기존 가까운 snapshot을 찾는다. - NewInternalDocumentFromSnapshot( ) -> snapshot을 이용해 doc 만든다. - 기존 스냅샷이 만약 없다면 synced vector map은 InitialSyncedVectorMap로 세팅 - 현재 snapshot ~ serverSeq 까지 change를 가져온다. -> doc 에 apply - CreateSnapshotInfo 를 통해 저장 > snapShot은 initialID를 이용해 synced vector map을 만들어 나간다. ### 4. 마이그레이션 - 문제다. ### 5. GC, Edit 기준 A. garbage collect 1. min synced vector(msv)를 구한다. - synced vector map의 각 actor의 min 값 2. 다음 기준에 따라 purge한다. ``` node.RemovedAt <= minTicket ``` > minTicket - Lamport : msv[node.RemovedAt.ActorID] - Delimiter : Max delimiter - ActorID : node.RemovedAt.ActorID B. Edit (deleteNodes) 1. Edit 연산시 actor의 vectorClock을 Edit operation에 포함 - vectorClock : Edit 연산의 lamport를 업데이트한 상태 2. Execute시 다음의 기준을 따라 노드를 keep ``` latestWhenEdit < node.CreatedAt ``` > latestWhenEdit (time ticket) - Lamport : vector[node.CreatedAt.ActorID] - Delimiter : Max delimiter - ActorID : editedAt.ActorID // 연산을 발생시킨 Actor ![image](https://hackmd.io/_uploads/SyZjCwksT.png) #### 6. On going - [x] text edit에서 삭제되는 부분의 latestCreatedAtMapByActor를 vector clock 이용하게 수정 - [ ] tree edit, style edit도 작업 - [x] gc_test 코드 수정 -> 기존 방식과 달리 gc가 조금 빨리 발생 - [x] snap shot -> document build 할 때 최신의 벡터로 초기화하도록 수정 - [ ] 기존 min synced ticket 부분 서버에서 걷어내기 - [ ] detach 케이스 고려 #### 7. synced vector map을 어떻게 저장할 것인가? | |200 | 300회 | | -------- | -------- | -------- | | json.marshal -> string | 136.229s | 396.534s | | json.marshal + detach | | 360.606s | | protobuf | 153.032s | 455.499s | - json.marshal -> string으로 저장 - 200회 ![image](https://hackmd.io/_uploads/HJNujmLja.png) - 300회 ![image](https://hackmd.io/_uploads/Sy_FJ4Uop.png) - protobuf -> []byte로 저장 - 200회 ![image](https://hackmd.io/_uploads/B1LtoXIiT.png) - 300회 ![image](https://hackmd.io/_uploads/SJapp7LjT.png) - json.marshal string + detach - 300회 ![image](https://hackmd.io/_uploads/rykHkcwsp.png)