github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/topic/topicwriterinternal/writer_single_stream.go (about) 1 package topicwriterinternal 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "reflect" 8 "sync/atomic" 9 10 "github.com/ydb-platform/ydb-go-sdk/v3/internal/background" 11 "github.com/ydb-platform/ydb-go-sdk/v3/internal/empty" 12 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon" 13 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicwriter" 14 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" 15 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" 16 "github.com/ydb-platform/ydb-go-sdk/v3/trace" 17 ) 18 19 var errSingleStreamWriterDoubleClose = xerrors.Wrap(errors.New("ydb: single stream writer impl double closed")) 20 21 type SingleStreamWriterConfig struct { 22 WritersCommonConfig 23 24 stream RawTopicWriterStream 25 queue *messageQueue 26 encodersMap *EncoderMap 27 getLastSeqNum bool 28 reconnectorInstanceID string 29 } 30 31 func newSingleStreamWriterConfig( 32 common WritersCommonConfig, //nolint:gocritic 33 stream RawTopicWriterStream, 34 queue *messageQueue, 35 encodersMap *EncoderMap, 36 getLastSeqNum bool, 37 reconnectorID string, 38 ) SingleStreamWriterConfig { 39 return SingleStreamWriterConfig{ 40 WritersCommonConfig: common, 41 stream: stream, 42 queue: queue, 43 encodersMap: encodersMap, 44 getLastSeqNum: getLastSeqNum, 45 reconnectorInstanceID: reconnectorID, 46 } 47 } 48 49 type SingleStreamWriter struct { 50 cfg SingleStreamWriterConfig 51 Encoder EncoderSelector 52 background background.Worker 53 CodecsFromServer rawtopiccommon.SupportedCodecs 54 allowedCodecs rawtopiccommon.SupportedCodecs 55 SessionID string 56 closeReason error 57 ReceivedLastSeqNum int64 58 PartitionID int64 59 closeCompleted empty.Chan 60 closed atomic.Bool 61 LastSeqNumRequested bool 62 } 63 64 func NewSingleStreamWriter( 65 ctxForPProfLabelsOnly context.Context, 66 cfg SingleStreamWriterConfig, //nolint:gocritic 67 ) (*SingleStreamWriter, error) { 68 res := newSingleStreamWriterStopped(ctxForPProfLabelsOnly, cfg) 69 70 if err := res.initStream(); err != nil { 71 _ = res.close(context.Background(), err) 72 73 return nil, err 74 } 75 76 res.start() 77 78 return res, nil 79 } 80 81 func newSingleStreamWriterStopped( 82 ctxForPProfLabelsOnly context.Context, 83 cfg SingleStreamWriterConfig, //nolint:gocritic 84 ) *SingleStreamWriter { 85 return &SingleStreamWriter{ 86 cfg: cfg, 87 background: *background.NewWorker( 88 xcontext.ValueOnly(ctxForPProfLabelsOnly), 89 "ydb-topic-stream-writer-background", 90 ), 91 closeCompleted: make(empty.Chan), 92 } 93 } 94 95 func (w *SingleStreamWriter) close(ctx context.Context, reason error) error { 96 if !w.closed.CompareAndSwap(false, true) { 97 return xerrors.WithStackTrace(errSingleStreamWriterDoubleClose) 98 } 99 100 defer close(w.closeCompleted) 101 w.closeReason = reason 102 103 resErr := w.cfg.stream.CloseSend() 104 bgWaitErr := w.background.Close(ctx, reason) 105 if resErr == nil { 106 resErr = bgWaitErr 107 } 108 109 return resErr 110 } 111 112 func (w *SingleStreamWriter) WaitClose(ctx context.Context) error { 113 select { 114 case <-ctx.Done(): 115 return ctx.Err() 116 case <-w.closeCompleted: 117 return w.closeReason 118 } 119 } 120 121 func (w *SingleStreamWriter) start() { 122 w.background.Start("topic writer update token", w.updateTokenLoop) 123 w.background.Start("topic writer send messages", w.sendMessagesFromQueueToStreamLoop) 124 w.background.Start("topic writer receive messages", w.receiveMessagesLoop) 125 } 126 127 func (w *SingleStreamWriter) initStream() (err error) { 128 traceOnDone := trace.TopicOnWriterInitStream(w.cfg.Tracer, w.cfg.reconnectorInstanceID, w.cfg.topic, w.cfg.producerID) 129 defer traceOnDone(w.SessionID, err) 130 131 req := w.createInitRequest() 132 if err = w.cfg.stream.Send(&req); err != nil { 133 return err 134 } 135 recvMessage, err := w.cfg.stream.Recv() 136 if err != nil { 137 return err 138 } 139 result, ok := recvMessage.(*rawtopicwriter.InitResult) 140 if !ok { 141 return xerrors.WithStackTrace( 142 fmt.Errorf("ydb: failed init response message type: %v", reflect.TypeOf(recvMessage)), 143 ) 144 } 145 146 w.allowedCodecs = calculateAllowedCodecs(w.cfg.forceCodec, w.cfg.encodersMap, result.SupportedCodecs) 147 if len(w.allowedCodecs) == 0 { 148 return xerrors.WithStackTrace(errNoAllowedCodecs) 149 } 150 151 w.Encoder = NewEncoderSelector( 152 w.cfg.encodersMap, 153 w.allowedCodecs, 154 w.cfg.compressorCount, 155 w.cfg.Tracer, 156 w.cfg.reconnectorInstanceID, 157 w.SessionID, 158 ) 159 160 w.SessionID = result.SessionID 161 w.LastSeqNumRequested = req.GetLastSeqNo 162 w.ReceivedLastSeqNum = result.LastSeqNo 163 w.PartitionID = result.PartitionID 164 w.CodecsFromServer = result.SupportedCodecs 165 166 return nil 167 } 168 169 func (w *SingleStreamWriter) createInitRequest() rawtopicwriter.InitRequest { 170 return rawtopicwriter.InitRequest{ 171 Path: w.cfg.topic, 172 ProducerID: w.cfg.producerID, 173 WriteSessionMeta: w.cfg.writerMeta, 174 Partitioning: w.cfg.defaultPartitioning, 175 GetLastSeqNo: w.cfg.getLastSeqNum, 176 } 177 } 178 179 func (w *SingleStreamWriter) receiveMessagesLoop(ctx context.Context) { 180 for { 181 if ctx.Err() != nil { 182 return 183 } 184 185 mess, err := w.cfg.stream.Recv() 186 if err != nil { 187 err = xerrors.WithStackTrace(fmt.Errorf("ydb: failed to receive message from write stream: %w", err)) 188 _ = w.close(ctx, err) 189 190 return 191 } 192 193 switch m := mess.(type) { 194 case *rawtopicwriter.WriteResult: 195 if err = w.cfg.queue.AcksReceived(m.Acks); err != nil && !errors.Is(err, errCloseClosedMessageQueue) { 196 reason := xerrors.WithStackTrace(err) 197 closeCtx, closeCtxCancel := xcontext.WithCancel(ctx) 198 closeCtxCancel() 199 _ = w.close(closeCtx, reason) 200 201 return 202 } 203 case *rawtopicwriter.UpdateTokenResponse: 204 // pass 205 default: 206 trace.TopicOnWriterReadUnknownGrpcMessage( 207 w.cfg.Tracer, 208 w.cfg.reconnectorInstanceID, 209 w.SessionID, 210 xerrors.WithStackTrace(xerrors.Wrap(fmt.Errorf( 211 "ydb: unexpected message type in stream reader: %v", 212 reflect.TypeOf(m), 213 ))), 214 ) 215 } 216 } 217 } 218 219 func (w *SingleStreamWriter) sendMessagesFromQueueToStreamLoop(ctx context.Context) { 220 for { 221 messages, err := w.cfg.queue.GetMessagesForSend(ctx) 222 if err != nil { 223 _ = w.close(ctx, err) 224 225 return 226 } 227 228 targetCodec, err := w.Encoder.CompressMessages(messages) 229 if err != nil { 230 _ = w.close(ctx, err) 231 232 return 233 } 234 235 onSentComplete := trace.TopicOnWriterSendMessages( 236 w.cfg.Tracer, 237 w.cfg.reconnectorInstanceID, 238 w.SessionID, 239 targetCodec.ToInt32(), 240 messages[0].SeqNo, 241 len(messages), 242 ) 243 err = sendMessagesToStream(w.cfg.stream, targetCodec, messages) 244 onSentComplete(err) 245 if err != nil { 246 err = xerrors.WithStackTrace(fmt.Errorf("ydb: error send message to topic stream: %w", err)) 247 _ = w.close(ctx, err) 248 249 return 250 } 251 } 252 } 253 254 func (w *SingleStreamWriter) updateTokenLoop(ctx context.Context) { 255 if ctx.Err() != nil { 256 return 257 } 258 259 ticker := w.cfg.clock.NewTicker(w.cfg.credUpdateInterval) 260 defer ticker.Stop() 261 262 ctxDone := ctx.Done() 263 tickerChan := ticker.Chan() 264 for { 265 select { 266 case <-ctxDone: 267 return 268 case <-tickerChan: 269 _ = w.sendUpdateToken(ctx) 270 } 271 } 272 } 273 274 func (w *SingleStreamWriter) sendUpdateToken(ctx context.Context) (err error) { 275 token, err := w.cfg.cred.Token(ctx) 276 if err != nil { 277 return err 278 } 279 280 stream := w.cfg.stream 281 if stream == nil { 282 // not connected yet 283 return nil 284 } 285 286 req := &rawtopicwriter.UpdateTokenRequest{} 287 req.Token = token 288 289 return stream.Send(req) 290 } 291 292 //go:generate mockgen -destination raw_topic_writer_stream_mock_test.go --typed -package topicwriterinternal -write_package_comment=false github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicwriterinternal RawTopicWriterStream 293 294 type RawTopicWriterStream interface { 295 Recv() (rawtopicwriter.ServerMessage, error) 296 Send(mess rawtopicwriter.ClientMessage) error 297 CloseSend() error 298 }