package notify import ( "context" "errors" "io" "net" "strings" "sync" "time" ) const ( BulkOpenSignalKey = "notify.bulk.open" BulkCloseSignalKey = "notify.bulk.close" BulkResetSignalKey = "notify.bulk.reset" BulkReleaseSignalKey = "notify.bulk.release" defaultBulkChunkSize = 1024 * 1024 defaultBulkInboundQueueLimit = 256 defaultBulkInboundBytesLimit = 64 * 1024 * 1024 defaultBulkOpenWindowBytes = 16 * 1024 * 1024 defaultBulkOpenMaxInFlight = 32 defaultBulkControlReadTimeout = 0 defaultBulkControlWriteTimeout = 0 ) type BulkMetadata map[string]string type BulkRange struct { Offset int64 Length int64 } type BulkOpenOptions struct { ID string Range BulkRange Metadata BulkMetadata ReadTimeout time.Duration WriteTimeout time.Duration Dedicated bool ChunkSize int WindowBytes int MaxInFlight int } type BulkAcceptInfo struct { ID string Range BulkRange Metadata BulkMetadata Dedicated bool LogicalConn *LogicalConn TransportConn *TransportConn TransportGeneration uint64 Bulk Bulk } type Bulk interface { io.Reader io.Writer io.Closer ID() string Range() BulkRange Metadata() BulkMetadata Context() context.Context LogicalConn() *LogicalConn TransportConn() *TransportConn TransportGeneration() uint64 CloseWrite() error Reset(error) error Snapshot() BulkSnapshot } var ( errBulkClientNil = errors.New("bulk client is nil") errBulkServerNil = errors.New("bulk server is nil") errBulkLogicalConnNil = errors.New("bulk logical connection is nil") errBulkTransportNil = errors.New("bulk transport connection is nil") errBulkRuntimeNil = errors.New("bulk runtime is nil") errBulkIDEmpty = errors.New("bulk id is empty") errBulkAlreadyExists = errors.New("bulk already exists") errBulkNotFound = errors.New("bulk not found") errBulkHandlerNotConfigured = errors.New("bulk handler is not configured") errBulkRejected = errors.New("bulk open rejected") errBulkReset = errors.New("bulk reset") errBulkDataIDEmpty = errors.New("bulk data id is empty") errBulkDataPathNotReady = errors.New("bulk data path is not implemented yet") errBulkRangeInvalid = errors.New("bulk range is invalid") errBulkBackpressureExceeded = errors.New("bulk inbound backpressure exceeded") errBulkDedicatedStreamOnly = errors.New("dedicated bulk requires stream transport") ) func clientDedicatedBulkSupportError(c *ClientCommon) error { if c == nil { return errBulkClientNil } if conn := c.clientTransportConnSnapshot(); conn != nil && isPacketTransportConn(conn) { return errBulkDedicatedStreamOnly } if source := c.clientConnectSourceSnapshot(); source != nil && source.isUDP() { return errBulkDedicatedStreamOnly } return nil } func logicalDedicatedBulkSupportError(logical *LogicalConn) error { if logical == nil { return errBulkLogicalConnNil } if transport := logical.CurrentTransportConn(); transport != nil { return transportDedicatedBulkSupportError(transport) } if addr := logical.RemoteAddr(); addr != nil && isPacketNetwork(addr.Network()) { return errBulkDedicatedStreamOnly } return nil } func transportDedicatedBulkSupportError(transport *TransportConn) error { if transport == nil { return errBulkTransportNil } if !transport.UsesStreamTransport() { return errBulkDedicatedStreamOnly } if addr := transport.RemoteAddr(); addr != nil && isPacketNetwork(addr.Network()) { return errBulkDedicatedStreamOnly } return nil } type bulkCloseSender func(context.Context, *bulkHandle, bool) error type bulkResetSender func(context.Context, *bulkHandle, string) error type bulkDataSender func(context.Context, *bulkHandle, []byte) error type bulkWriteSender func(context.Context, *bulkHandle, []byte) (int, error) type bulkReleaseSender func(*bulkHandle, int64, int) error type bulkHandle struct { runtime *bulkRuntime runtimeScope string id string dataID uint64 outboundSeq uint64 rangeSpec BulkRange metadata BulkMetadata sessionEpoch uint64 client *ClientCommon logical *LogicalConn transport *TransportConn transportGeneration uint64 readTimeout time.Duration writeTimeout time.Duration dedicated bool dedicatedAttachToken string chunkSize int windowBytes int maxInFlight int inboundQueueLimit int inboundBytesLimit int closeFn bulkCloseSender resetFn bulkResetSender sendDataFn bulkDataSender sendWriteFn bulkWriteSender releaseFn bulkReleaseSender ctx context.Context cancel context.CancelFunc createdAt time.Time writeMu sync.Mutex mu sync.Mutex localClosed bool localReadClosed bool remoteClosed bool peerReadClosed bool resetErr error readQueue [][]byte readBuf []byte bufferedBytes int readNotify chan struct{} flowNotify chan struct{} pendingReleaseBytes int64 pendingReleaseChunks int outboundAvailBytes int64 outboundInFlight int bytesRead int64 bytesWritten int64 readCalls int64 writeCalls int64 lastReadAt time.Time lastWriteAt time.Time dedicatedMu sync.Mutex dedicatedConn net.Conn dedicatedSender *bulkDedicatedSender dedicatedReady chan struct{} dedicatedWriteClosed bool acceptMu sync.Mutex acceptDispatched bool } func newBulkHandle(parent context.Context, runtime *bulkRuntime, runtimeScope string, req BulkOpenRequest, sessionEpoch uint64, logical *LogicalConn, transport *TransportConn, transportGeneration uint64, closeFn bulkCloseSender, resetFn bulkResetSender, sendDataFn bulkDataSender, sendWriteFn bulkWriteSender, releaseFn bulkReleaseSender) *bulkHandle { if parent == nil { parent = context.Background() } ctx, cancel := context.WithCancel(parent) if transportGeneration == 0 && transport != nil { transportGeneration = transport.TransportGeneration() } if transportGeneration == 0 && logical != nil { transportGeneration = logical.transportGenerationSnapshot() } req = normalizeBulkOpenRequest(req) return &bulkHandle{ runtime: runtime, runtimeScope: runtimeScope, id: req.BulkID, dataID: req.DataID, rangeSpec: req.Range, metadata: cloneBulkMetadata(req.Metadata), sessionEpoch: sessionEpoch, logical: logical, transport: transport, transportGeneration: transportGeneration, readTimeout: req.ReadTimeout, writeTimeout: req.WriteTimeout, dedicated: req.Dedicated, dedicatedAttachToken: req.AttachToken, chunkSize: req.ChunkSize, windowBytes: req.WindowBytes, maxInFlight: req.MaxInFlight, inboundQueueLimit: defaultBulkInboundQueueLimit, inboundBytesLimit: defaultBulkInboundBytesLimit, closeFn: closeFn, resetFn: resetFn, sendDataFn: sendDataFn, sendWriteFn: sendWriteFn, releaseFn: releaseFn, ctx: ctx, cancel: cancel, createdAt: time.Now(), readNotify: make(chan struct{}, 1), flowNotify: make(chan struct{}, 1), dedicatedReady: make(chan struct{}), outboundAvailBytes: int64(req.WindowBytes), } } func (b *bulkHandle) ID() string { if b == nil { return "" } return b.id } func (b *bulkHandle) Range() BulkRange { if b == nil { return BulkRange{} } return b.rangeSpec } func (b *bulkHandle) Metadata() BulkMetadata { if b == nil { return nil } return cloneBulkMetadata(b.metadata) } func (b *bulkHandle) Context() context.Context { if b == nil || b.ctx == nil { return context.Background() } return b.ctx } func (b *bulkHandle) LogicalConn() *LogicalConn { if b == nil { return nil } return b.logical } func (b *bulkHandle) TransportConn() *TransportConn { if b == nil { return nil } return b.transport } func (b *bulkHandle) TransportGeneration() uint64 { if b == nil { return 0 } return b.transportGeneration } func (b *bulkHandle) Dedicated() bool { if b == nil { return false } return b.dedicated } func (b *bulkHandle) dedicatedAttachTokenSnapshot() string { if b == nil { return "" } b.mu.Lock() defer b.mu.Unlock() return b.dedicatedAttachToken } func (b *bulkHandle) setDedicatedAttachToken(token string) { if b == nil { return } b.mu.Lock() b.dedicatedAttachToken = token b.mu.Unlock() } func (b *bulkHandle) dedicatedConnSnapshot() net.Conn { if b == nil { return nil } b.dedicatedMu.Lock() defer b.dedicatedMu.Unlock() return b.dedicatedConn } func (b *bulkHandle) dedicatedSenderSnapshot() *bulkDedicatedSender { if b == nil { return nil } b.dedicatedMu.Lock() defer b.dedicatedMu.Unlock() return b.dedicatedSender } func (b *bulkHandle) installDedicatedSender(sender *bulkDedicatedSender) *bulkDedicatedSender { if b == nil || sender == nil { return nil } b.dedicatedMu.Lock() defer b.dedicatedMu.Unlock() if b.dedicatedSender != nil { return b.dedicatedSender } b.dedicatedSender = sender return sender } func (b *bulkHandle) clearDedicatedSender() *bulkDedicatedSender { if b == nil { return nil } b.dedicatedMu.Lock() defer b.dedicatedMu.Unlock() sender := b.dedicatedSender b.dedicatedSender = nil return sender } func (b *bulkHandle) dedicatedAttachedSnapshot() bool { return b.dedicatedConnSnapshot() != nil } func (b *bulkHandle) waitDedicatedReady(ctx context.Context) error { if b == nil || !b.Dedicated() || b.dedicatedAttachedSnapshot() { return nil } if ctx == nil { ctx = context.Background() } select { case <-b.dedicatedReady: return nil case <-ctx.Done(): return ctx.Err() case <-b.Context().Done(): if err := b.writeStateErrorSnapshot(); err != nil { return err } return context.Canceled } } func (b *bulkHandle) attachDedicatedConn(conn net.Conn) error { if b == nil { return io.ErrClosedPipe } if conn == nil { return net.ErrClosed } b.dedicatedMu.Lock() if b.dedicatedConn != nil { b.dedicatedMu.Unlock() return errors.New("bulk dedicated conn already attached") } b.dedicatedConn = conn b.dedicatedWriteClosed = false ready := b.dedicatedReady b.dedicatedMu.Unlock() if ready != nil { select { case <-ready: default: close(ready) } } return nil } func (b *bulkHandle) bestEffortCloseDedicatedWriteHalf() { if b == nil || !b.dedicated { return } b.dedicatedMu.Lock() conn := b.dedicatedConn alreadyClosed := b.dedicatedWriteClosed b.dedicatedMu.Unlock() if conn == nil || alreadyClosed { return } type closeWriter interface { CloseWrite() error } if closeWriterConn, ok := conn.(closeWriter); ok { if err := closeWriterConn.CloseWrite(); err == nil { b.dedicatedMu.Lock() if b.dedicatedConn == conn { b.dedicatedWriteClosed = true } b.dedicatedMu.Unlock() } } } func (b *bulkHandle) dedicatedWriteHalfClosedSnapshot() bool { if b == nil { return false } b.dedicatedMu.Lock() defer b.dedicatedMu.Unlock() return b.dedicatedWriteClosed } func (b *bulkHandle) setClientSnapshotOwner(client *ClientCommon) { if b == nil { return } b.client = client } func (b *bulkHandle) clearDedicatedConn() net.Conn { if b == nil { return nil } b.dedicatedMu.Lock() conn := b.dedicatedConn b.dedicatedConn = nil b.dedicatedWriteClosed = false b.dedicatedMu.Unlock() return conn } func (b *bulkHandle) markAcceptDispatched() bool { if b == nil { return false } b.acceptMu.Lock() defer b.acceptMu.Unlock() if b.acceptDispatched { return false } b.acceptDispatched = true return true } func (b *bulkHandle) SessionEpoch() uint64 { if b == nil { return 0 } return b.sessionEpoch } func (b *bulkHandle) acceptsClientSessionEpoch(epoch uint64) bool { if b == nil { return false } if b.sessionEpoch == 0 || epoch == 0 { return true } return b.sessionEpoch == epoch } func (b *bulkHandle) acceptsTransportGeneration(transport *TransportConn) bool { if b == nil { return false } if b.transportGeneration == 0 || transport == nil { return true } return b.transportGeneration == transport.TransportGeneration() } func (b *bulkHandle) dataIDSnapshot() uint64 { if b == nil { return 0 } return b.dataID } func (b *bulkHandle) nextOutboundDataSeq() uint64 { if b == nil { return 0 } b.mu.Lock() defer b.mu.Unlock() b.outboundSeq++ return b.outboundSeq } func (b *bulkHandle) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if b == nil { return 0, io.ErrClosedPipe } for { b.mu.Lock() localReadClosed := b.localReadClosed if len(b.readBuf) > 0 { n := copy(p, b.readBuf) b.readBuf = b.readBuf[n:] b.bufferedBytes -= n if b.bufferedBytes < 0 { b.bufferedBytes = 0 } b.recordReadLocked(n, time.Now()) b.mu.Unlock() b.maybeSendWindowRelease(n, false) return n, nil } if len(b.readQueue) > 0 { b.readBuf = b.readQueue[0] b.readQueue[0] = nil b.readQueue = b.readQueue[1:] b.mu.Unlock() continue } resetErr := b.resetErr remoteClosed := b.remoteClosed notify := b.readNotify ctx := b.ctx readTimeout := b.readTimeout b.mu.Unlock() if localReadClosed { b.maybeSendWindowRelease(0, true) return 0, io.ErrClosedPipe } if resetErr != nil { b.maybeSendWindowRelease(0, true) return 0, resetErr } if remoteClosed { b.maybeSendWindowRelease(0, true) return 0, io.EOF } if err := b.waitReadable(ctx, notify, readTimeout); err != nil { b.maybeSendWindowRelease(0, true) return 0, err } } } func (b *bulkHandle) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if b == nil { return 0, io.ErrClosedPipe } b.writeMu.Lock() defer b.writeMu.Unlock() b.mu.Lock() resetErr := b.resetErr localClosed := b.localClosed peerReadClosed := b.peerReadClosed sendDataFn := b.sendDataFn sendWriteFn := b.sendWriteFn chunkSize := b.chunkSize writeTimeout := b.writeTimeout bulkCtx := b.ctx b.mu.Unlock() if resetErr != nil { return 0, resetErr } if localClosed || peerReadClosed { return 0, io.ErrClosedPipe } if sendDataFn == nil { return 0, errBulkDataPathNotReady } if b.dedicated && sendWriteFn != nil { written := 0 for written < len(p) { end := len(p) if b.windowBytes > 0 && end-written > b.windowBytes { end = written + b.windowBytes } part := p[written:end] sendCtx, cancel, err := bulkWriteContext(bulkCtx, writeTimeout) if err != nil { if written > 0 { b.recordWrite(written, time.Now()) } return written, err } if err := b.acquireOutboundWindow(sendCtx, len(part)); err != nil { cancel() if written > 0 { b.recordWrite(written, time.Now()) } return written, b.normalizeWriteError(err) } partWritten, err := sendWriteFn(sendCtx, b, part) cancel() if partWritten < 0 { partWritten = 0 } if partWritten > len(part) { partWritten = len(part) } if partWritten < len(part) { b.rollbackOutboundWindow(len(part) - partWritten) } written += partWritten if err != nil { if written > 0 { b.recordWrite(written, time.Now()) } return written, b.normalizeWriteError(err) } if partWritten != len(part) { if written > 0 { b.recordWrite(written, time.Now()) } return written, io.ErrShortWrite } } if written > 0 { b.recordWrite(written, time.Now()) } return written, nil } if chunkSize <= 0 { chunkSize = defaultBulkChunkSize } written := 0 for written < len(p) { end := written + chunkSize if end > len(p) { end = len(p) } chunk := p[written:end] sendCtx, cancel, err := bulkWriteContext(bulkCtx, writeTimeout) if err != nil { if written > 0 { b.recordWrite(written, time.Now()) } return written, err } if err := b.acquireOutboundWindow(sendCtx, len(chunk)); err != nil { cancel() if written > 0 { b.recordWrite(written, time.Now()) } return written, b.normalizeWriteError(err) } err = sendDataFn(sendCtx, b, chunk) cancel() if err != nil { b.rollbackOutboundWindow(len(chunk)) if written > 0 { b.recordWrite(written, time.Now()) } return written, b.normalizeWriteError(err) } written = end } if written > 0 { b.recordWrite(written, time.Now()) } return written, nil } func (b *bulkHandle) Close() error { return b.close(true) } func (b *bulkHandle) CloseWrite() error { return b.close(false) } func (b *bulkHandle) close(full bool) error { if b == nil { return nil } b.writeMu.Lock() defer b.writeMu.Unlock() b.mu.Lock() if b.resetErr != nil { err := b.resetErr b.mu.Unlock() return err } if b.localClosed { if !full || b.localReadClosed { b.mu.Unlock() return nil } closeFn := b.closeFn b.mu.Unlock() if closeFn != nil && !b.dedicatedWriteHalfClosedSnapshot() { if err := closeFn(context.Background(), b, true); err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) { return err } } b.bestEffortCloseDedicatedWriteHalf() b.mu.Lock() if b.localReadClosed { b.mu.Unlock() return nil } b.localReadClosed = true b.clearBufferedDataLocked() shouldFinalize := b.shouldFinalizeLocked() b.mu.Unlock() b.notifyReadable() if shouldFinalize { b.finalize() } return nil } closeFn := b.closeFn b.mu.Unlock() if closeFn != nil { if err := closeFn(context.Background(), b, full); err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) { return err } } b.bestEffortCloseDedicatedWriteHalf() b.mu.Lock() if b.localClosed { b.mu.Unlock() return nil } b.localClosed = true if full { b.localReadClosed = true b.clearBufferedDataLocked() } shouldFinalize := b.shouldFinalizeLocked() b.mu.Unlock() if full { b.notifyReadable() } if shouldFinalize { b.finalize() } return nil } func (b *bulkHandle) Reset(err error) error { if b == nil { return nil } resetErr := bulkResetError(err) b.mu.Lock() if b.resetErr != nil { err := b.resetErr b.mu.Unlock() return err } resetFn := b.resetFn b.mu.Unlock() if resetFn != nil { if sendErr := resetFn(context.Background(), b, bulkResetMessage(resetErr)); sendErr != nil { return sendErr } } b.markReset(resetErr) return nil } func (b *bulkHandle) Snapshot() BulkSnapshot { return b.snapshot() } func (b *bulkHandle) markRemoteClosed() { if b == nil { return } b.mu.Lock() b.remoteClosed = true shouldFinalize := b.shouldFinalizeLocked() b.mu.Unlock() b.notifyReadable() if shouldFinalize { b.finalize() } } func (b *bulkHandle) markPeerClosed() { if b == nil { return } b.mu.Lock() b.remoteClosed = true b.peerReadClosed = true shouldFinalize := b.shouldFinalizeLocked() b.notifyFlowLocked() b.mu.Unlock() b.notifyReadable() if shouldFinalize { b.finalize() } } func (b *bulkHandle) markReset(err error) { if b == nil { return } b.mu.Lock() if b.resetErr == nil { b.resetErr = bulkResetError(err) b.clearBufferedDataLocked() } b.notifyFlowLocked() b.mu.Unlock() b.notifyReadable() b.finalize() } func (b *bulkHandle) pushChunk(chunk []byte) error { return b.pushChunkWithOwnership(chunk, false) } func (b *bulkHandle) pushOwnedChunk(chunk []byte) error { return b.pushChunkWithOwnership(chunk, true) } func (b *bulkHandle) pushOwnedChunkNoReset(chunk []byte) error { return b.pushChunkWithOwnershipOptions(chunk, true, false) } func (b *bulkHandle) pushChunkWithOwnership(chunk []byte, owned bool) error { return b.pushChunkWithOwnershipOptions(chunk, owned, true) } func (b *bulkHandle) pushChunkWithOwnershipOptions(chunk []byte, owned bool, resetOnOverflow bool) error { if b == nil { return io.ErrClosedPipe } if len(chunk) == 0 { return nil } stored := chunk if !owned { stored = append([]byte(nil), chunk...) } b.mu.Lock() if b.resetErr != nil { err := b.resetErr b.mu.Unlock() return err } if b.inboundQueueLimit > 0 && b.bufferedChunkCountLocked() >= b.inboundQueueLimit { if !resetOnOverflow { b.mu.Unlock() return errBulkBackpressureExceeded } err := b.markResetLocked(errBulkBackpressureExceeded) b.mu.Unlock() b.notifyReadable() b.finalize() return err } if b.inboundBytesLimit > 0 && b.bufferedBytes+len(stored) > b.inboundBytesLimit { if !resetOnOverflow { b.mu.Unlock() return errBulkBackpressureExceeded } err := b.markResetLocked(errBulkBackpressureExceeded) b.mu.Unlock() b.notifyReadable() b.finalize() return err } b.readQueue = append(b.readQueue, stored) b.bufferedBytes += len(stored) b.notifyReadableLocked() b.mu.Unlock() return nil } func (b *bulkHandle) markResetLocked(err error) error { if b == nil { return io.ErrClosedPipe } if b.resetErr == nil { b.resetErr = bulkResetError(err) b.clearBufferedDataLocked() } return b.resetErr } func (b *bulkHandle) clearBufferedDataLocked() { if b == nil { return } for i := range b.readQueue { b.readQueue[i] = nil } b.readQueue = nil b.readBuf = nil b.bufferedBytes = 0 } func (b *bulkHandle) flowControlEnabled() bool { if b == nil { return false } return b.releaseFn != nil && (b.windowBytes > 0 || b.maxInFlight > 0) } func (b *bulkHandle) releaseThresholdBytes() int64 { if b == nil { return int64(defaultBulkChunkSize) } threshold := b.chunkSize if threshold <= 0 { threshold = defaultBulkChunkSize } if b.windowBytes > 0 && threshold > b.windowBytes { threshold = b.windowBytes } if threshold <= 0 { threshold = defaultBulkChunkSize } return int64(threshold) } func (b *bulkHandle) maybeSendWindowRelease(consumed int, force bool) { if b == nil || !b.flowControlEnabled() { return } var ( bytes int64 chunks int release bulkReleaseSender ) b.mu.Lock() if consumed > 0 { b.pendingReleaseBytes += int64(consumed) b.pendingReleaseChunks++ } if !force && b.pendingReleaseBytes < b.releaseThresholdBytes() { b.mu.Unlock() return } bytes = b.pendingReleaseBytes chunks = b.pendingReleaseChunks release = b.releaseFn b.pendingReleaseBytes = 0 b.pendingReleaseChunks = 0 b.mu.Unlock() if release != nil && (bytes > 0 || chunks > 0) { _ = release(b, bytes, chunks) } } func (b *bulkHandle) acquireOutboundWindow(ctx context.Context, size int) error { if b == nil || size <= 0 || !b.flowControlEnabled() { return nil } if ctx == nil { ctx = context.Background() } need := int64(size) for { b.mu.Lock() if b.resetErr != nil { err := b.resetErr b.mu.Unlock() return err } if b.localClosed || b.peerReadClosed { b.mu.Unlock() return io.ErrClosedPipe } bytesOK := true if b.windowBytes > 0 { bytesOK = b.outboundAvailBytes >= need if !bytesOK && need > int64(b.windowBytes) && b.outboundInFlight == 0 { bytesOK = true } } chunksOK := true if b.maxInFlight > 0 { chunksOK = b.outboundInFlight < b.maxInFlight } if bytesOK && chunksOK { if b.windowBytes > 0 { b.outboundAvailBytes -= need } if b.maxInFlight > 0 { b.outboundInFlight++ } b.mu.Unlock() return nil } notify := b.flowNotify b.mu.Unlock() select { case <-notify: case <-ctx.Done(): if stateErr := b.writeStateErrorSnapshot(); stateErr != nil { return stateErr } return normalizeStreamDeadlineError(ctx.Err()) } } } func (b *bulkHandle) rollbackOutboundWindow(size int) { if b == nil || size <= 0 || !b.flowControlEnabled() { return } b.mu.Lock() if b.windowBytes > 0 { b.outboundAvailBytes += int64(size) maxAvail := int64(b.windowBytes) if b.outboundAvailBytes > maxAvail { b.outboundAvailBytes = maxAvail } } if b.maxInFlight > 0 && b.outboundInFlight > 0 { b.outboundInFlight-- } b.notifyFlowLocked() b.mu.Unlock() } func (b *bulkHandle) releaseOutboundWindow(bytes int64, chunks int) { if b == nil || !b.flowControlEnabled() { return } b.mu.Lock() if b.windowBytes > 0 && bytes > 0 { b.outboundAvailBytes += bytes maxAvail := int64(b.windowBytes) if b.outboundAvailBytes > maxAvail { b.outboundAvailBytes = maxAvail } } if b.maxInFlight > 0 && chunks > 0 { b.outboundInFlight -= chunks if b.outboundInFlight < 0 { b.outboundInFlight = 0 } } b.notifyFlowLocked() b.mu.Unlock() } func (b *bulkHandle) bufferedChunkCountLocked() int { if b == nil { return 0 } count := len(b.readQueue) if len(b.readBuf) > 0 { count++ } return count } func (b *bulkHandle) shouldFinalizeLocked() bool { if b == nil { return true } if b.resetErr != nil { return true } if b.dedicated { return (b.peerReadClosed && b.remoteClosed) || (b.localClosed && b.remoteClosed) } return b.localReadClosed || (b.peerReadClosed && b.remoteClosed) || (b.localClosed && b.remoteClosed) } func (b *bulkHandle) snapshot() BulkSnapshot { if b == nil { return BulkSnapshot{} } dedicatedAttached := b.dedicatedAttachedSnapshot() b.mu.Lock() defer b.mu.Unlock() snapshot := BulkSnapshot{ ID: b.id, DataID: b.dataID, Scope: normalizeFileScope(b.runtimeScope), Range: b.rangeSpec, Metadata: cloneBulkMetadata(b.metadata), Dedicated: b.dedicated, DedicatedAttached: dedicatedAttached, SessionEpoch: b.sessionEpoch, TransportGeneration: b.transportGeneration, LocalClosed: b.localClosed, LocalReadClosed: b.localReadClosed, RemoteClosed: b.remoteClosed, PeerReadClosed: b.peerReadClosed, BufferedChunks: b.bufferedChunkCountLocked(), BufferedBytes: b.bufferedBytes, ReadTimeout: b.readTimeout, WriteTimeout: b.writeTimeout, ChunkSize: b.chunkSize, WindowBytes: b.windowBytes, MaxInFlight: b.maxInFlight, BytesRead: b.bytesRead, BytesWritten: b.bytesWritten, ReadCalls: b.readCalls, WriteCalls: b.writeCalls, OpenedAt: b.createdAt, LastReadAt: b.lastReadAt, LastWriteAt: b.lastWriteAt, } if b.logical != nil { snapshot.LogicalClientID = b.logical.ID() } if b.resetErr != nil { snapshot.ResetError = b.resetErr.Error() } var diag snapshotBindingDiagnostics switch { case b.logical != nil || b.transport != nil: diag = snapshotBindingDiagnosticsFromLogical(b.logical, b.transport, b.transportGeneration) case b.client != nil: diag = snapshotBindingDiagnosticsFromClient(b.client, b.sessionEpoch) } snapshot.BindingOwner = diag.BindingOwner snapshot.BindingAlive = diag.BindingAlive snapshot.BindingCurrent = diag.BindingCurrent snapshot.BindingReason = diag.BindingReason snapshot.BindingError = diag.BindingError snapshot.TransportAttached = diag.TransportAttached snapshot.TransportHasRuntimeConn = diag.TransportHasRuntimeConn snapshot.TransportCurrent = diag.TransportCurrent snapshot.TransportDetachReason = diag.TransportDetachReason snapshot.TransportDetachKind = diag.TransportDetachKind snapshot.TransportDetachGeneration = diag.TransportDetachGeneration snapshot.TransportDetachError = diag.TransportDetachError snapshot.TransportDetachedAt = diag.TransportDetachedAt snapshot.ReattachEligible = diag.ReattachEligible return snapshot } func (b *bulkHandle) finalize() { if b == nil { return } b.maybeSendWindowRelease(0, true) if b.cancel != nil { b.cancel() } if sender := b.clearDedicatedSender(); sender != nil { sender.stop() } if conn := b.clearDedicatedConn(); conn != nil { _ = conn.Close() } if b.runtime != nil { b.runtime.remove(b.runtimeScope, b.id) } } func (b *bulkHandle) recordReadLocked(n int, now time.Time) { if b == nil || n <= 0 { return } b.bytesRead += int64(n) b.readCalls++ b.lastReadAt = now } func (b *bulkHandle) recordWrite(n int, now time.Time) { if b == nil || n <= 0 { return } b.mu.Lock() b.bytesWritten += int64(n) b.writeCalls++ b.lastWriteAt = now b.mu.Unlock() } func (b *bulkHandle) waitReadable(ctx context.Context, notify <-chan struct{}, timeout time.Duration) error { if ctx == nil { ctx = context.Background() } deadline := streamEffectiveDeadline(time.Now(), timeout, time.Time{}) if deadline.IsZero() { select { case <-notify: return nil case <-ctx.Done(): if resetErr := b.resetErrSnapshot(); resetErr != nil { return resetErr } if b.localReadClosedSnapshot() { return io.ErrClosedPipe } if b.remoteClosedSnapshot() { return nil } return ctx.Err() } } if !deadline.After(time.Now()) { return normalizeStreamDeadlineError(context.DeadlineExceeded) } timer := time.NewTimer(time.Until(deadline)) defer timer.Stop() select { case <-notify: return nil case <-ctx.Done(): if resetErr := b.resetErrSnapshot(); resetErr != nil { return resetErr } if b.localReadClosedSnapshot() { return io.ErrClosedPipe } if b.remoteClosedSnapshot() { return nil } return normalizeStreamDeadlineError(ctx.Err()) case <-timer.C: return normalizeStreamDeadlineError(context.DeadlineExceeded) } } func (b *bulkHandle) resetErrSnapshot() error { if b == nil { return io.ErrClosedPipe } b.mu.Lock() defer b.mu.Unlock() return b.resetErr } func (b *bulkHandle) remoteClosedSnapshot() bool { if b == nil { return true } b.mu.Lock() defer b.mu.Unlock() return b.remoteClosed } func (b *bulkHandle) localClosedSnapshot() bool { if b == nil { return true } b.mu.Lock() defer b.mu.Unlock() return b.localClosed } func (b *bulkHandle) localReadClosedSnapshot() bool { if b == nil { return true } b.mu.Lock() defer b.mu.Unlock() return b.localReadClosed } func (b *bulkHandle) writeStateErrorSnapshot() error { if b == nil { return io.ErrClosedPipe } b.mu.Lock() defer b.mu.Unlock() if b.resetErr != nil { return b.resetErr } if b.localClosed || b.peerReadClosed { return io.ErrClosedPipe } return nil } func (b *bulkHandle) notifyReadable() { if b == nil { return } b.mu.Lock() defer b.mu.Unlock() b.notifyReadableLocked() } func (b *bulkHandle) notifyReadableLocked() { if b == nil || b.readNotify == nil { return } select { case b.readNotify <- struct{}{}: default: } } func (b *bulkHandle) notifyFlowLocked() { if b == nil || b.flowNotify == nil { return } select { case b.flowNotify <- struct{}{}: default: } } func (b *bulkHandle) normalizeWriteError(err error) error { if err == nil { return nil } if stateErr := b.writeStateErrorSnapshot(); stateErr != nil { return stateErr } return normalizeStreamDeadlineError(err) } func (b *bulkHandle) canIgnoreDedicatedCloseSendError(err error) bool { if b == nil || !b.dedicated || err == nil { return false } b.mu.Lock() defer b.mu.Unlock() if !(b.ctx.Err() != nil || b.remoteClosed || b.peerReadClosed || b.localClosed) { return false } if errors.Is(err, errTransportDetached) || errors.Is(err, net.ErrClosed) || errors.Is(err, io.ErrClosedPipe) { return true } message := strings.ToLower(err.Error()) return strings.Contains(message, "broken pipe") || strings.Contains(message, "use of closed network connection") } func bulkWriteContext(parent context.Context, timeout time.Duration) (context.Context, func(), error) { if parent == nil { parent = context.Background() } deadline := streamEffectiveDeadline(time.Now(), timeout, time.Time{}) if !deadline.IsZero() && !deadline.After(time.Now()) { return nil, func() {}, normalizeStreamDeadlineError(context.DeadlineExceeded) } if deadline.IsZero() { ctx, cancel := context.WithCancel(parent) return ctx, cancel, nil } ctx, cancel := context.WithDeadline(parent, deadline) return ctx, cancel, nil } func normalizeBulkOpenRequest(req BulkOpenRequest) BulkOpenRequest { req.Range = normalizeBulkRange(req.Range) req.Metadata = cloneBulkMetadata(req.Metadata) if req.ChunkSize <= 0 { req.ChunkSize = defaultBulkChunkSize } if req.WindowBytes <= 0 { req.WindowBytes = defaultBulkOpenWindowBytes } if req.MaxInFlight <= 0 { req.MaxInFlight = defaultBulkOpenMaxInFlight } if req.ReadTimeout < 0 { req.ReadTimeout = defaultBulkControlReadTimeout } if req.WriteTimeout < 0 { req.WriteTimeout = defaultBulkControlWriteTimeout } return req } func normalizeBulkOpenOptions(opt BulkOpenOptions) BulkOpenOptions { req := normalizeBulkOpenRequest(BulkOpenRequest{ BulkID: opt.ID, Range: opt.Range, Metadata: opt.Metadata, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, Dedicated: opt.Dedicated, ChunkSize: opt.ChunkSize, WindowBytes: opt.WindowBytes, MaxInFlight: opt.MaxInFlight, }) return BulkOpenOptions{ ID: req.BulkID, Range: req.Range, Metadata: req.Metadata, ReadTimeout: req.ReadTimeout, WriteTimeout: req.WriteTimeout, Dedicated: req.Dedicated, ChunkSize: req.ChunkSize, WindowBytes: req.WindowBytes, MaxInFlight: req.MaxInFlight, } } func normalizeBulkRange(r BulkRange) BulkRange { if r.Offset < 0 { r.Offset = -1 } if r.Length < 0 { r.Length = -1 } return r } func validBulkRange(r BulkRange) bool { return r.Offset >= 0 && r.Length >= 0 } func cloneBulkMetadata(src BulkMetadata) BulkMetadata { if len(src) == 0 { return nil } dst := make(BulkMetadata, len(src)) for key, value := range src { dst[key] = value } return dst } func bulkResetError(err error) error { if err == nil { return errBulkReset } return err } func bulkResetMessage(err error) string { if err == nil { return "" } return err.Error() }