@@ -23,6 +23,7 @@ import (
2323 "net/http"
2424 "net/url"
2525 "strings"
26+ "sync"
2627 "time"
2728
2829 gapic "cloud.google.com/go/storage/internal/apiv2"
@@ -185,7 +186,8 @@ func (c *grpcStorageClient) OpenWriter(params *openWriterParams, opts ...storage
185186 appendGen : params .appendGen ,
186187 finalizeOnClose : params .finalizeOnClose ,
187188
188- buf : make ([]byte , 0 , chunkSize ),
189+ buf : nil , // Allocated lazily on first buffered write.
190+ chunkSize : chunkSize ,
189191 writeQuantum : writeQuantum ,
190192 lastSegmentStart : lastSegmentStart ,
191193 sendableUnits : sendableUnits ,
@@ -267,7 +269,8 @@ type gRPCWriter struct {
267269 appendGen int64
268270 finalizeOnClose bool
269271
270- buf []byte
272+ buf []byte
273+ chunkSize int
271274 // A writeQuantum is the largest quantity of data which can be sent to the
272275 // service in a single message.
273276 writeQuantum int
@@ -384,21 +387,26 @@ func (w *gRPCWriter) gatherFirstBuffer() error {
384387 for cmd := range w .writesChan {
385388 switch v := cmd .(type ) {
386389 case * gRPCWriterCommandWrite :
387- if len (w .buf )+ len (v .p ) <= cap (w .buf ) {
388- // We have not started sending yet, and we can stage all data without
389- // starting a send. Compare against cap(w.buf) instead of
390- // w.writeQuantum: that way we can perform a oneshot upload for objects
391- // which fit in one chunk, even though we will cut the request into
392- // w.writeQuantum units when we do start sending.
393- origLen := len (w .buf )
394- w .buf = w .buf [:origLen + len (v .p )]
395- copy (w .buf [origLen :], v .p )
396- close (v .done )
397- } else {
398- // Too large. Handle it in writeLoop.
390+ // If zero-copy one-shot is requested, OR the payload is larger than the buffer,
391+ // bypass buffering entirely and hand off to the writeLoop immediately.
392+ if w .forceOneShot || len (w .buf )+ len (v .p ) > w .chunkSize {
399393 w .currentCommand = cmd
400394 return nil
401395 }
396+
397+ // Otherwise, lazily allocate and stage the small write (normal buffered path)
398+ if w .buf == nil {
399+ w .buf = make ([]byte , 0 , w .chunkSize )
400+ }
401+ // We have not started sending yet, and we can stage all data without
402+ // starting a send. Compare against w.chunkSize instead of
403+ // w.writeQuantum: that way we can perform a oneshot upload for objects
404+ // which fit in one chunk, even though we will cut the request into
405+ // w.writeQuantum units when we do start sending.
406+ origLen := len (w .buf )
407+ w .buf = w .buf [:origLen + len (v .p )]
408+ copy (w .buf [origLen :], v .p )
409+ close (v .done )
402410 break
403411 case * gRPCWriterCommandClose :
404412 // If we get here, data (if any) fits in w.buf, so we can force oneshot.
@@ -568,17 +576,33 @@ type gRPCWriterCommand interface {
568576}
569577
570578type gRPCWriterCommandWrite struct {
571- p []byte
572- done chan struct {}
579+ p []byte
580+ done chan struct {}
581+ initialOffset int64
582+ hasStarted bool
583+ closeOnce sync.Once
573584}
574585
575586func (c * gRPCWriterCommandWrite ) handle (w * gRPCWriter , cs gRPCWriterCommandHandleChans ) error {
576587 if len (c .p ) == 0 {
577588 // No data to write.
578- close (c .done )
589+ c .markDone ()
590+ return nil
591+ }
592+
593+ // Zero-Copy send.
594+ if w .forceOneShot {
595+ err := c .zeroCopyWrite (w , cs )
596+ if err != nil {
597+ return err
598+ }
599+ // If zeroCopyWrite returns without error, the write is done.
579600 return nil
580601 }
581602
603+ if w .buf == nil {
604+ w .buf = make ([]byte , 0 , w .chunkSize )
605+ }
582606 wblen := len (w .buf )
583607 allKnownBytes := wblen + len (c .p )
584608 fullBufs := allKnownBytes / cap (w .buf )
@@ -605,7 +629,7 @@ func (c *gRPCWriterCommandWrite) handle(w *gRPCWriter, cs gRPCWriterCommandHandl
605629 return w .streamSender .err ()
606630 }
607631 w .bufUnsentIdx = int (sentOffset - w .bufBaseOffset )
608- close ( c . done )
632+ c . markDone ( )
609633 return nil
610634 }
611635
@@ -698,10 +722,53 @@ func (c *gRPCWriterCommandWrite) handle(w *gRPCWriter, cs gRPCWriterCommandHandl
698722 w .buf = w .buf [:len (toCopyIn )]
699723 copy (w .buf , toCopyIn )
700724 w .bufUnsentIdx = int (sentOffset - w .bufBaseOffset )
701- close (c .done )
725+ c .markDone ()
726+ return nil
727+ }
728+
729+ func (c * gRPCWriterCommandWrite ) zeroCopyWrite (w * gRPCWriter , cs gRPCWriterCommandHandleChans ) error {
730+ // Pre-emptively get the context channel to avoid closure overhead in the loop.
731+ ctxDone := w .preRunCtx .Done ()
732+
733+ // sendBufferToTarget handles the quantum breakdown.
734+ newOffset , ok := w .sendBufferToTarget (cs , c .p , w .bufBaseOffset , len (c .p ), w .handleCompletion )
735+ if ! ok {
736+ return w .streamSender .err ()
737+ }
738+
739+ // Request an ack from the sender goroutine to ensure the buffer has been
740+ // dispatched to gRPC and is safe for the user to reuse.
741+ if ! cs .deliverRequestUnlessCompleted (gRPCBidiWriteRequest {requestAck : true }, w .handleCompletion ) {
742+ return w .streamSender .err ()
743+ }
744+
745+ ackOutstanding := true
746+
747+ // Wait for server acknowledgement and sender transmissions to enable incremental progress.
748+ for ackOutstanding || w .bufBaseOffset < newOffset {
749+ select {
750+ case completion , ok := <- cs .completions :
751+ if ! ok {
752+ return w .streamSender .err ()
753+ }
754+ w .handleCompletion (completion )
755+ case <- cs .requestAcks :
756+ ackOutstanding = false
757+ case <- ctxDone :
758+ return w .preRunCtx .Err ()
759+ }
760+ }
761+
762+ c .p = nil
763+ c .markDone ()
702764 return nil
703765}
704766
767+ // Helper to ensure we don't close done twice and keep the main logic clean.
768+ func (c * gRPCWriterCommandWrite ) markDone () {
769+ c .closeOnce .Do (func () { close (c .done ) })
770+ }
771+
705772type gRPCWriterCommandFlush struct {
706773 done chan int64
707774}
0 commit comments