diff --git a/server/backend/database/mongo/client.go b/server/backend/database/mongo/client.go index de70d1308..3de8c6fac 100644 --- a/server/backend/database/mongo/client.go +++ b/server/backend/database/mongo/client.go @@ -28,6 +28,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/gridfs" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" @@ -43,7 +44,8 @@ import ( const ( // StatusKey is the key of the status field. - StatusKey = "status" + StatusKey = "status" + BSONMaxSnapshotSize = 16 * 1024 * 1024 // 16MB ) // Client is a client that connects to Mongo DB and reads or saves Yorkie data. @@ -1073,16 +1075,52 @@ func (c *Client) CreateSnapshotInfo( return err } - if _, err := c.collection(ColSnapshots).InsertOne(ctx, bson.M{ - "project_id": docRefKey.ProjectID, - "doc_id": docRefKey.DocID, - "server_seq": doc.Checkpoint().ServerSeq, - "lamport": doc.Lamport(), - "version_vector": doc.VersionVector(), - "snapshot": snapshot, - "created_at": gotime.Now(), - }); err != nil { - return fmt.Errorf("insert snapshot: %w", err) + if len(snapshot) > BSONMaxSnapshotSize { + db := c.client.Database(c.config.YorkieDatabase) + + // create GridFS bucket + bucket, err := gridfs.NewBucket(db) + if err != nil { + return fmt.Errorf("failed to create GridFS bucket: %w", err) + } + + uploadStream, err := bucket.OpenUploadStream(fmt.Sprintf("%s_snapshot", docRefKey.DocID)) + if err != nil { + return fmt.Errorf("failed to open GridFS upload stream: %w", err) + } + defer uploadStream.Close() + + _, err = uploadStream.Write(snapshot) + if err != nil { + return fmt.Errorf("failed to write to GridFS: %w", err) + } + + fileID := uploadStream.FileID + + if _, err := c.collection(ColSnapshots).InsertOne(ctx, bson.M{ + "project_id": docRefKey.ProjectID, + "doc_id": docRefKey.DocID, + "server_seq": doc.Checkpoint().ServerSeq, + "lamport": doc.Lamport(), + "version_vector": doc.VersionVector(), + "snapshot_file_id": fileID, // GridFS file ID + "created_at": gotime.Now(), + }); err != nil { + return fmt.Errorf("insert snapshot info: %w", err) + } + + } else { + if _, err := c.collection(ColSnapshots).InsertOne(ctx, bson.M{ + "project_id": docRefKey.ProjectID, + "doc_id": docRefKey.DocID, + "server_seq": doc.Checkpoint().ServerSeq, + "lamport": doc.Lamport(), + "version_vector": doc.VersionVector(), + "snapshot": snapshot, + "created_at": gotime.Now(), + }); err != nil { + return fmt.Errorf("insert snapshot: %w", err) + } } return nil diff --git a/server/backend/database/testcases/testcases.go b/server/backend/database/testcases/testcases.go index 8f363db2f..43d958766 100644 --- a/server/backend/database/testcases/testcases.go +++ b/server/backend/database/testcases/testcases.go @@ -1619,3 +1619,33 @@ func AssertKeys(t *testing.T, expectedKeys []key.Key, infos []*database.DocInfo) } assert.EqualValues(t, expectedKeys, keys) } + +func RunCreateLargeSnapshotTest(t *testing.T, db database.Database, projectID types.ID) { + t.Run("store and validate large snapshot test", func(t *testing.T) { + ctx := context.Background() + docKey := key.Key(fmt.Sprintf("tests$%s", t.Name())) + + clientInfo, _ := db.ActivateClient(ctx, projectID, t.Name()) + bytesID, _ := clientInfo.ID.Bytes() + actorID, _ := time.ActorIDFromBytes(bytesID) + docInfo, _ := db.FindDocInfoByKeyAndOwner(ctx, clientInfo.RefKey(), docKey, true) + + doc := document.New(docKey) + doc.SetActor(actorID) + + largeData := make([]byte, 16*1024*1024+1) + for i := range largeData { + largeData[i] = byte('A' + (i % 26)) + } + + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + root.SetBytes("largeField", largeData) + return nil + })) + + docRefKey := docInfo.RefKey() + + err := db.CreateSnapshotInfo(ctx, docRefKey, doc.InternalDocument()) + assert.NoError(t, err) + }) +} diff --git a/test/complex/mongo_client_test.go b/test/complex/mongo_client_test.go index 2fd6691fb..27d203406 100644 --- a/test/complex/mongo_client_test.go +++ b/test/complex/mongo_client_test.go @@ -171,4 +171,8 @@ func TestClientWithShardedDB(t *testing.T) { assert.Equal(t, docInfo1.Key, result.Key) assert.Equal(t, docInfo1.ID, result.ID) }) + + t.Run("CreateLargeSnapshotTest test", func(t *testing.T) { + testcases.CreateLargeSnapshotTest(t, cli, dummyProjectID) + }) }