@@ -2,12 +2,14 @@ package table
22
33import (
44 "context"
5+ "fmt"
56
67 "github.com/jonboulle/clockwork"
78 "github.com/ydb-platform/ydb-go-genproto/Ydb_Table_V1"
89 "github.com/ydb-platform/ydb-go-genproto/protos/Ydb"
910 "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Table"
1011 "google.golang.org/grpc"
12+ "google.golang.org/protobuf/proto"
1113
1214 "github.com/ydb-platform/ydb-go-sdk/v3/internal/pool"
1315 "github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
@@ -299,7 +301,7 @@ func (c *Client) BulkUpsert(
299301 retry .WithTrace (& trace.Retry {
300302 OnRetry : func (info trace.RetryLoopStartInfo ) func (trace.RetryLoopDoneInfo ) {
301303 return func (info trace.RetryLoopDoneInfo ) {
302- attempts = info .Attempts
304+ attempts += max ( info .Attempts - 1 , 0 ) // `max` guarded against negative values
303305 }
304306 },
305307 }),
@@ -309,32 +311,111 @@ func (c *Client) BulkUpsert(
309311 stack .FunctionID ("github.com/ydb-platform/ydb-go-sdk/v3/internal/table.(*Client).BulkUpsert" ),
310312 )
311313 defer func () {
312- onDone (finalErr , attempts )
314+ onDone (finalErr , attempts + 1 )
313315 }()
314316
315317 request , err := data .ToYDB (tableName )
316318 if err != nil {
317319 return xerrors .WithStackTrace (err )
318320 }
319321
322+ chunks := make ([]* Ydb_Table.BulkUpsertRequest , 0 , 1 )
323+
324+ // We must send requests in chunks to avoid exceeding the maximum message size
325+ chunks , err = chunkBulkUpsertRequest (chunks , request , c .config .MaxRequestMessageSize ())
326+ if err != nil {
327+ return err
328+ }
329+
330+ return c .sendBulkUpsertRequest (ctx , chunks , config .RetryOptions ... )
331+ }
332+
333+ func (c * Client ) sendBulkUpsertRequest (
334+ ctx context.Context ,
335+ requests []* Ydb_Table.BulkUpsertRequest ,
336+ retryOpts ... retry.Option ,
337+ ) error {
320338 client := Ydb_Table_V1 .NewTableServiceClient (c .cc )
321339
322- err = retry . Retry ( ctx ,
323- func ( ctx context. Context ) ( err error ) {
324- attempts ++
325- _ , err = client .BulkUpsert (ctx , request )
340+ for _ , request := range requests {
341+ err := retry . Retry ( ctx ,
342+ func ( ctx context. Context ) ( err error ) {
343+ _ , err = client .BulkUpsert (ctx , request )
326344
327- return err
328- },
329- config .RetryOptions ... ,
330- )
331- if err != nil {
332- return xerrors .WithStackTrace (err )
345+ return err
346+ },
347+ retryOpts ... ,
348+ )
349+ if err != nil {
350+ return xerrors .WithStackTrace (err )
351+ }
333352 }
334353
335354 return nil
336355}
337356
357+ // chunkBulkUpsertRequest splits a bulk upsert request into smaller chunks if it exceeds the maximum message size.
358+ // It recursively divides the request into smaller parts, ensuring each chunk is within the size limit.
359+ // Returns a slice of chunked bulk upsert requests or an error if the request cannot be split.
360+ func chunkBulkUpsertRequest (
361+ dst []* Ydb_Table.BulkUpsertRequest ,
362+ req * Ydb_Table.BulkUpsertRequest ,
363+ maxBytes int ,
364+ ) ([]* Ydb_Table.BulkUpsertRequest , error ) {
365+ reqSize := proto .Size (req )
366+
367+ // not exceed the maximum size -> ret original request
368+ if reqSize <= maxBytes {
369+ return append (dst , req ), nil
370+ }
371+
372+ // not a row bulk upsert request -> ret original request
373+ if req .GetRows () == nil || req .GetRows ().GetValue () == nil {
374+ return nil , xerrors .WithStackTrace (
375+ xerrors .Wrap (
376+ fmt .Errorf ("ydb: request size (%d bytes) exceeds maximum size (%d bytes) " +
377+ " but cannot be chunked (only row-based bulk upserts support chunking)" , reqSize , maxBytes )))
378+ }
379+
380+ n := len (req .GetRows ().GetValue ().GetItems ())
381+ if n == 0 {
382+ return dst , nil
383+ }
384+
385+ // we cannot split one item and one item is too big
386+ if n == 1 {
387+ return nil , xerrors .WithStackTrace (
388+ xerrors .Wrap (
389+ fmt .Errorf ("ydb: single row size (%d bytes) exceeds maximum request size (%d bytes) " +
390+ "- row is too large to process" , reqSize , maxBytes )))
391+ }
392+
393+ left , right := splitBulkUpsertRequestAt (req , n / 2 )
394+
395+ dst , err := chunkBulkUpsertRequest (dst , left , maxBytes )
396+ if err != nil {
397+ return nil , err
398+ }
399+
400+ return chunkBulkUpsertRequest (dst , right , maxBytes )
401+ }
402+
403+ // splitBulkUpsertRequestAt splits a bulk upsert request into two parts at the specified position.
404+ // It divides the request's items into two separate requests, with the first request containing
405+ // items from the start up to the specified position, and the second request containing the remaining items.
406+ // Returns two modified bulk upsert requests with their respective item sets.
407+ func splitBulkUpsertRequestAt (req * Ydb_Table.BulkUpsertRequest , pos int ) (_ , _ * Ydb_Table.BulkUpsertRequest ) {
408+ items := req .GetRows ().GetValue ().GetItems () // save original items
409+ req .Rows .Value .Items = nil
410+
411+ right := proto .Clone (req ).(* Ydb_Table.BulkUpsertRequest ) //nolint:forcetypeassert
412+
413+ req .Rows .Value .Items = items [:pos ]
414+ right .Rows .Value .Items = items [pos :]
415+
416+ return req , right
417+ }
418+
338419func makeReadRowsRequest (
339420 sessionID string ,
340421 path string ,
0 commit comments