|
| 1 | +# Thrift Sink in Go |
| 2 | + |
| 3 | +This document explains how to use Thrift Sink in Go, both from the client side |
| 4 | +and server side. |
| 5 | + |
| 6 | +For general information about Thrift Sink concepts and how to define sink |
| 7 | +services in Thrift IDL, please refer to the |
| 8 | +[Thrift Streaming Wiki](https://www.internalfb.com/intern/staticdocs/thrift/docs/fb/features/streaming/). |
| 9 | + |
| 10 | +## Table of Contents |
| 11 | + |
| 12 | +- [Overview](#overview) |
| 13 | +- [Defining Sink Services](#defining-sink-services) |
| 14 | +- [Client-Side Usage](#client-side-usage) |
| 15 | +- [Server-Side Implementation](#server-side-implementation) |
| 16 | + |
| 17 | +## Overview |
| 18 | + |
| 19 | +Thrift Sink allows the client to stream data to the server (client-to-server |
| 20 | +streaming), enabling efficient handling of large uploads or continuous data |
| 21 | +feeds. The client produces a stream of elements that the server consumes, and |
| 22 | +the server returns a final response after processing all elements. Go's |
| 23 | +implementation uses `iter.Seq2` iterators to provide an idiomatic API for sink |
| 24 | +functionality. |
| 25 | + |
| 26 | +**Key differences from Streaming:** |
| 27 | + |
| 28 | +| Feature | Streaming | Sink | |
| 29 | +| ----------------- | --------------------- | --------------------- | |
| 30 | +| Data direction | Server → Client | Client → Server | |
| 31 | +| Producer | Server | Client | |
| 32 | +| Consumer | Client | Server | |
| 33 | +| Final response | None | Server sends after consuming all elements | |
| 34 | + |
| 35 | +## Defining Sink Services |
| 36 | + |
| 37 | +Define sink methods in your Thrift IDL file using the `sink<ElemType, FinalResponseType>` |
| 38 | +return type: |
| 39 | + |
| 40 | +```thrift |
| 41 | +// Sink-only (no initial response) |
| 42 | +service DataService { |
| 43 | + sink<i32, i32> SinkOnly(); |
| 44 | +} |
| 45 | +
|
| 46 | +// Response and sink (initial response followed by sink) |
| 47 | +service FileService { |
| 48 | + i32 /* upload id */, sink<binary /* chunk */, i64 /* checksum */> UploadFileChunks(1: string fileName); |
| 49 | +} |
| 50 | +
|
| 51 | +// Sink with declared exception on sink elements |
| 52 | +service ProcessingService { |
| 53 | + sink<Data throws (1: ProcessingException ex), Result> ProcessData(); |
| 54 | +} |
| 55 | +
|
| 56 | +// Sink with declared exception on final response |
| 57 | +service ValidationService { |
| 58 | + sink<Data, Result throws (1: ValidationException ex)> ValidateData(); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +## Client-Side Usage |
| 63 | + |
| 64 | +### API Signature |
| 65 | + |
| 66 | +The generated client API returns a callback function that accepts an `iter.Seq2[ElemType, error]` |
| 67 | +iterator (the sink producer) and returns the final response. The values returned |
| 68 | +depend on whether the sink has an initial response: |
| 69 | + |
| 70 | +**Sink-only (no initial response):** |
| 71 | + |
| 72 | +```go |
| 73 | +func (c *Client) SinkMethod(ctx context.Context, args...) ( |
| 74 | + func(iter.Seq2[ElemType, error]) (FinalResponse, error), // Sink callback |
| 75 | + error, // Initial error |
| 76 | +) |
| 77 | +``` |
| 78 | + |
| 79 | +**Response and sink (with initial response):** |
| 80 | + |
| 81 | +```go |
| 82 | +func (c *Client) SinkMethod(ctx context.Context, args...) ( |
| 83 | + *InitialResponse, // First response |
| 84 | + func(iter.Seq2[ElemType, error]) (FinalResponse, error), // Sink callback |
| 85 | + error, // Initial error |
| 86 | +) |
| 87 | +``` |
| 88 | + |
| 89 | +### Usage Guidelines |
| 90 | + |
| 91 | +1. **Context is Required**: The API REQUIRES a context with a timeout, deadline, |
| 92 | + or manual cancel to ensure background goroutines are terminated (avoid |
| 93 | + leaks). |
| 94 | + |
| 95 | +2. **Check Initial Error**: Always check the initial error first. If non-nil, no |
| 96 | + sink follows. |
| 97 | + |
| 98 | +3. **Create a Producer Function**: Define an `iter.Seq2[ElemType, error]` |
| 99 | + function that yields elements to send to the server. |
| 100 | + |
| 101 | +4. **Call the Sink Callback**: Pass your producer function to the sink callback |
| 102 | + to start streaming elements to the server. |
| 103 | + |
| 104 | +5. **Handle Final Response**: The sink callback returns the final response from |
| 105 | + the server after all elements have been consumed. |
| 106 | + |
| 107 | +6. **Cleanup**: You should NOT worry about cleaning up resources besides |
| 108 | + providing a reasonable context timeout/cancellation. |
| 109 | + |
| 110 | +### Example: Sink-Only |
| 111 | + |
| 112 | +```go |
| 113 | +func main() { |
| 114 | + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 115 | + defer cancel() |
| 116 | + |
| 117 | + sinkCallback, err := client.SinkOnly(ctx) |
| 118 | + if err != nil { |
| 119 | + log.Fatalf("request failed: %v", err) |
| 120 | + } |
| 121 | + |
| 122 | + // Define the producer function that yields elements |
| 123 | + sinkSeq := func(yield func(int32, error) bool) { |
| 124 | + for i := int32(1); i <= 5; i++ { |
| 125 | + if !yield(i, nil) { |
| 126 | + return // Stop if consumer signals to stop |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + // Call the sink callback with the producer |
| 132 | + finalResponse, err := sinkCallback(sinkSeq) |
| 133 | + if err != nil { |
| 134 | + log.Fatalf("sink failed: %v", err) |
| 135 | + } |
| 136 | + |
| 137 | + fmt.Printf("Final response: %d\n", finalResponse) |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### Example: Response and Sink |
| 142 | + |
| 143 | +```go |
| 144 | +func main() { |
| 145 | + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 146 | + defer cancel() |
| 147 | + |
| 148 | + uploadID, sinkCallback, err := client.UploadFileChunks(ctx, "myfile.txt") |
| 149 | + if err != nil { |
| 150 | + log.Fatalf("request failed: %v", err) |
| 151 | + } |
| 152 | + |
| 153 | + fmt.Printf("Upload ID: %d\n", uploadID) |
| 154 | + |
| 155 | + // Open file and create producer |
| 156 | + file, err := os.Open("myfile.txt") |
| 157 | + if err != nil { |
| 158 | + log.Fatalf("failed to open file: %v", err) |
| 159 | + } |
| 160 | + defer file.Close() |
| 161 | + |
| 162 | + sinkSeq := func(yield func([]byte, error) bool) { |
| 163 | + buffer := make([]byte, 4096) |
| 164 | + for { |
| 165 | + n, err := file.Read(buffer) |
| 166 | + if err == io.EOF { |
| 167 | + return // Done reading |
| 168 | + } |
| 169 | + if err != nil { |
| 170 | + yield(nil, err) // Signal error to server |
| 171 | + return |
| 172 | + } |
| 173 | + if !yield(buffer[:n], nil) { |
| 174 | + return // Stop if consumer signals to stop |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + checksum, err := sinkCallback(sinkSeq) |
| 180 | + if err != nil { |
| 181 | + log.Fatalf("upload failed: %v", err) |
| 182 | + } |
| 183 | + |
| 184 | + fmt.Printf("Upload complete. Checksum: %d\n", checksum) |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +### Example: Handling Errors in Producer |
| 189 | + |
| 190 | +```go |
| 191 | +sinkSeq := func(yield func(Data, error) bool) { |
| 192 | + for _, item := range items { |
| 193 | + data, err := processItem(item) |
| 194 | + if err != nil { |
| 195 | + // Signal error to server - this will be received |
| 196 | + // as the error in the server's iter.Seq2 loop |
| 197 | + yield(Data{}, err) |
| 198 | + return |
| 199 | + } |
| 200 | + if !yield(data, nil) { |
| 201 | + return |
| 202 | + } |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +## Server-Side Implementation |
| 208 | + |
| 209 | +### API Signature |
| 210 | + |
| 211 | +Server-side sink methods have a different signature that returns a consumer |
| 212 | +function: |
| 213 | + |
| 214 | +**Sink-only (no initial response):** |
| 215 | + |
| 216 | +```go |
| 217 | +func (h *Handler) SinkMethod(ctx context.Context, args...) ( |
| 218 | + func(context.Context, iter.Seq2[ElemType, error]) (FinalResponse, error), // Consumer function |
| 219 | + error, // Initial error |
| 220 | +) |
| 221 | +``` |
| 222 | + |
| 223 | +**Response and sink (with initial response):** |
| 224 | + |
| 225 | +```go |
| 226 | +func (h *Handler) SinkMethod(ctx context.Context, args...) ( |
| 227 | + *InitialResponse, // First response |
| 228 | + func(context.Context, iter.Seq2[ElemType, error]) (FinalResponse, error), // Consumer function |
| 229 | + error, // Initial error |
| 230 | +) |
| 231 | +``` |
| 232 | + |
| 233 | +### Implementation Guidelines |
| 234 | + |
| 235 | +1. **Return Initial Response/Error First**: Return the initial response and/or |
| 236 | + error before sink processing begins. |
| 237 | + |
| 238 | +2. **Consumer Function**: Return a consumer function that will be invoked by the |
| 239 | + Thrift library to process sink elements. |
| 240 | + |
| 241 | +3. **Use Context for Cancellation**: The context passed to the consumer function |
| 242 | + should be used to detect stream interruption (e.g. client disconnected). |
| 243 | + |
| 244 | +4. **Iterate Over Elements**: Use Go's `for elem, err := range seq` syntax to |
| 245 | + consume elements from the client. |
| 246 | + |
| 247 | +5. **Handle Client Errors**: Check the error value in each iteration. If non-nil, |
| 248 | + the client encountered an error and stopped sending. |
| 249 | + |
| 250 | +6. **Return Final Response**: After consuming all elements, return the final |
| 251 | + response to the client. |
| 252 | + |
| 253 | +7. **Return Error for Sink Errors**: Return an error from the consumer function |
| 254 | + to signal a final response error to the client. |
| 255 | + |
| 256 | +8. **Cleanup Resources**: The consumer function should clean up all resources |
| 257 | + before returning. |
| 258 | + |
| 259 | +### Example: Sink-Only |
| 260 | + |
| 261 | +```go |
| 262 | +type DataService struct{} |
| 263 | + |
| 264 | +func (s *DataService) SinkOnly(ctx context.Context) (func(context.Context, iter.Seq2[int32, error]) (int32, error), error) { |
| 265 | + // Return consumer function |
| 266 | + elemConsumerFunc := func(ctx context.Context, seq iter.Seq2[int32, error]) (int32, error) { |
| 267 | + var sum int32 |
| 268 | + for elem, err := range seq { |
| 269 | + // Check for client-side error |
| 270 | + if err != nil { |
| 271 | + return sum, err |
| 272 | + } |
| 273 | + // Check for cancellation |
| 274 | + select { |
| 275 | + case <-ctx.Done(): |
| 276 | + return sum, ctx.Err() |
| 277 | + default: |
| 278 | + } |
| 279 | + // Process element |
| 280 | + sum += elem |
| 281 | + } |
| 282 | + return sum, nil // Return final response |
| 283 | + } |
| 284 | + |
| 285 | + return elemConsumerFunc, nil |
| 286 | +} |
| 287 | +``` |
| 288 | + |
| 289 | +### Example: Response and Sink |
| 290 | + |
| 291 | +```go |
| 292 | +type FileService struct{} |
| 293 | + |
| 294 | +func (h *FileService) UploadFileChunks(ctx context.Context, fileName string) ( |
| 295 | + int32, |
| 296 | + func(context.Context, iter.Seq2[[]byte, error]) (int64, error), |
| 297 | + error, |
| 298 | +) { |
| 299 | + // Create upload and get ID for initial response |
| 300 | + uploadID, err := createUpload(fileName) |
| 301 | + if err != nil { |
| 302 | + return 0, nil, fmt.Errorf("failed to create upload: %w", err) |
| 303 | + } |
| 304 | + |
| 305 | + // Create consumer function |
| 306 | + elemConsumerFunc := func(ctx context.Context, seq iter.Seq2[[]byte, error]) (int64, error) { |
| 307 | + var checksum int64 |
| 308 | + |
| 309 | + for chunk, err := range seq { |
| 310 | + // Check for client-side error |
| 311 | + if err != nil { |
| 312 | + cancelUpload(uploadID) |
| 313 | + return 0, err |
| 314 | + } |
| 315 | + |
| 316 | + // Check for cancellation |
| 317 | + select { |
| 318 | + case <-ctx.Done(): |
| 319 | + cancelUpload(uploadID) |
| 320 | + return 0, ctx.Err() |
| 321 | + default: |
| 322 | + } |
| 323 | + |
| 324 | + // Process chunk |
| 325 | + if err := writeChunk(uploadID, chunk); err != nil { |
| 326 | + cancelUpload(uploadID) |
| 327 | + return 0, fmt.Errorf("failed to write chunk: %w", err) |
| 328 | + } |
| 329 | + checksum = computeChecksum(checksum, chunk) |
| 330 | + } |
| 331 | + |
| 332 | + // Finalize upload |
| 333 | + if err := finalizeUpload(uploadID); err != nil { |
| 334 | + return 0, fmt.Errorf("failed to finalize upload: %w", err) |
| 335 | + } |
| 336 | + |
| 337 | + return checksum, nil |
| 338 | + } |
| 339 | + |
| 340 | + return uploadID, elemConsumerFunc, nil |
| 341 | +} |
| 342 | +``` |
| 343 | + |
| 344 | +### Example: Returning a Final Response Exception |
| 345 | + |
| 346 | +```go |
| 347 | +func (h *Handler) ValidateData(ctx context.Context) (func(context.Context, iter.Seq2[*Data, error]) (*Result, error), error) { |
| 348 | + elemConsumerFunc := func(ctx context.Context, seq iter.Seq2[*Data, error]) (*Result, error) { |
| 349 | + for data, err := range seq { |
| 350 | + if err != nil { |
| 351 | + return nil, err |
| 352 | + } |
| 353 | + if !isValid(data) { |
| 354 | + // Return declared exception as final response error |
| 355 | + return nil, NewValidationException().SetMessage("invalid data") |
| 356 | + } |
| 357 | + } |
| 358 | + return &Result{Success: true}, nil |
| 359 | + } |
| 360 | + |
| 361 | + return elemConsumerFunc, nil |
| 362 | +} |
| 363 | +``` |
| 364 | + |
| 365 | +### Example: Handling Initial Exception |
| 366 | + |
| 367 | +```go |
| 368 | +func (h *Handler) ProcessData(ctx context.Context) (func(context.Context, iter.Seq2[*Data, error]) (*Result, error), error) { |
| 369 | + if !h.isReady() { |
| 370 | + // Return initial error - sink will not proceed |
| 371 | + return nil, NewServiceException().SetMessage("service not ready") |
| 372 | + } |
| 373 | + |
| 374 | + elemConsumerFunc := func(ctx context.Context, seq iter.Seq2[*Data, error]) (*Result, error) { |
| 375 | + // Process elements... |
| 376 | + for data, err := range seq { |
| 377 | + if err != nil { |
| 378 | + return nil, err |
| 379 | + } |
| 380 | + process(data) |
| 381 | + } |
| 382 | + return &Result{Success: true}, nil |
| 383 | + } |
| 384 | + |
| 385 | + return elemConsumerFunc, nil |
| 386 | +} |
| 387 | +``` |
| 388 | + |
| 389 | +## Additional Resources |
| 390 | + |
| 391 | +- [General Thrift Streaming Documentation](https://www.internalfb.com/intern/staticdocs/thrift/docs/fb/features/streaming/) |
| 392 | +- [Thrift Streaming in Go](streaming.md) |
0 commit comments