-
Notifications
You must be signed in to change notification settings - Fork 512
Description
As of now, the flow of spans from Span Processor to Exporter is synchronous, which means the processor has to wait for the upload/export status of the ongoing request before invoking a new request.
Application -> Span::OnEnd() -> SimpleProcessor::OnEnd -> Mutex Lock -> Exporter::Export()
The application has to wait for the span to be uploaded to continue further with business logic.
The Batch processor simplifies this by batching/caching the spans in the processor and uploading them in a separate thread.
Application -> Span::OnEnd() -> BatchProcessor::OnEnd() -> CircularBuffer:Add(span)
-> Span::OnEnd() -> BatchProcessor::OnEnd() -> CircularBuffer:Add(span)
-> Span::OnEnd() -> BatchProcessor::OnEnd() -> CircularBuffer:Add(span)
ProcessorThread-> CircularBuffer::Drain() -> Export::Export()
Application -> Span::OnEnd() -> BatchProcessor::OnEnd() -> CircularBuffer:Add(span)
This allows the Application to continue processing business logic without waiting for spans to export, and also Span::OnEnd() will not block even when export is ongoing in a separate thread ( there is no lock due to the use of CircularBuffer to store Spans).
With BatchProcessor, while the interface between application and processor is fast (no wait), the interface between processor and exporter is still synchronous. As the processor needs to wait for the ongoing span(s) export to finish before invoking new export. This causes a bottleneck if the application is creating spans faster than the time taken by the exporter to export the spans, and eventually, the CircularBuffer gets full. Once the CircularBuffer gets full, Span::OnEnd() needs to wait/block on the buffer to be available for writing.
The current design for the interface between processor and exporter is driven by specification as below
- Export() will never be called concurrently for the same exporter instance. Export() can be called again only after the current call returns.
- Success - The batch has been successfully exported. For protocol exporters this typically means that the data is sent over the wire and delivered to the destination server.
- Note: this result may be returned via an async mechanism or a callback, if that is idiomatic for the language implementation.
So, the solution could be to return the exporter::Export() result as a callback or async mechanism,
- Async mechanism: C++ provide async mechanism using std::async/std::promise. It would be good to do more research if its is suitable for more complex scenarios, and whether all the compiler provide a stable implementation for it before deciding on it ( Good read - https://www.cppstories.com/2014/01/tasks-with-stdfuture-and-stdasync/ )
- Callback mechanism: would be much simpler something like:
bool Exporter:Export(const nostd::span<std::unique_ptr<Recordable>> &spans,
nostd::function_ref<bool(ExportResult)> result_callback);This allows the exporter to invoke the result_callback when the export status is available, and the processor to invoke the export without waiting for the ongoing export to complete.
Also, the interface change should be backward compatible, and shouldn't break the existing/external exporters. One option would be to have the processor decide which export mechanism to use based on the configurable parameter. The default should be as existing ( i.e, synchronous export).