import time
import threading
from dataclasses import dataclass, field
from typing import Callable, Dict, List
@dataclass
class MetricPoint:
name: str
value: float
timestamp: float = field(default_factory=time.time)
tags: Dict[str, str] = field(default_factory=dict)
def as_dict(self) -> dict:
return {
"name": self.name,
"value": self.value,
"ts": self.timestamp,
"tags": self.tags,
}
class MetricBuffer:
def __init__(self, sink: Callable[[List[MetricPoint]], None], max_size: int = 500):
self._sink = sink
self._max_size = max_size
self._lock = threading.Lock()
self._points: List[MetricPoint] = []
def record(self, point: MetricPoint) -> bool:
with self._lock:
self._points.append(point)
return len(self._points) >= self._max_size
def flush(self) -> int:
with self._lock:
batch, self._points = self._points, []
if not batch:
return 0
return self._deliver(batch)
def _deliver(self, batch: List[MetricPoint]) -> int:
try:
self._sink(batch)
return len(batch)
except Exception:
# Re-queue at the front for at-least-once delivery.
with self._lock:
self._points = batch + self._points
raise
import time
import threading
from typing import Callable, Dict, List
from metrics_buffer import MetricBuffer, MetricPoint
class BatchingMetricsClient:
def __init__(self, sink, max_size: int = 500, flush_interval: float = 5.0):
self._buffer = MetricBuffer(sink, max_size=max_size)
self._flush_interval = flush_interval
self._stop = threading.Event()
self._worker = threading.Thread(target=self._run, name="metrics-flush", daemon=True)
self._worker.start()
def gauge(self, name: str, value: float, tags: Dict[str, str] = None) -> None:
self._emit(MetricPoint(name=name, value=value, tags=tags or {}))
def increment(self, name: str, amount: float = 1.0, tags: Dict[str, str] = None) -> None:
self._emit(MetricPoint(name=name, value=amount, tags=tags or {}))
def _emit(self, point: MetricPoint) -> None:
if self._buffer.record(point):
# Size trigger: don't wait for the timer.
self._safe_flush()
def _run(self) -> None:
while not self._stop.is_set():
self._stop.wait(self._flush_interval)
self._safe_flush()
def _safe_flush(self) -> None:
try:
self._buffer.flush()
except Exception:
pass # kept buffered; retried on next tick
def close(self, timeout: float = 5.0) -> None:
self._stop.set()
self._worker.join(timeout=timeout)
self._safe_flush()
from typing import List
import requests
from metrics_buffer import MetricPoint
class HttpMetricSink:
def __init__(self, endpoint: str, api_key: str, timeout: float = 3.0):
self._endpoint = endpoint
self._timeout = timeout
self._session = requests.Session()
self._session.headers.update({"Authorization": f"Bearer {api_key}"})
def __call__(self, batch: List[MetricPoint]) -> None:
payload = {"series": [p.as_dict() for p in batch]}
resp = self._session.post(self._endpoint, json=payload, timeout=self._timeout)
if resp.status_code >= 300:
raise RuntimeError(f"metric sink rejected batch: {resp.status_code} {resp.text[:200]}")
import threading
from metrics_buffer import MetricBuffer, MetricPoint
class RecordingSink:
def __init__(self):
self.batches = []
self._lock = threading.Lock()
def __call__(self, batch):
with self._lock:
self.batches.append(list(batch))
def test_size_trigger_forces_flush():
sink = RecordingSink()
buffer = MetricBuffer(sink, max_size=3)
full = False
for i in range(3):
full = buffer.record(MetricPoint(name="req.count", value=i))
assert full is True
assert buffer.flush() == 3
assert len(sink.batches) == 1
assert len(sink.batches[0]) == 3
def test_failed_sink_requeues_batch():
class FlakySink:
def __init__(self):
self.calls = 0
def __call__(self, batch):
self.calls += 1
if self.calls == 1:
raise RuntimeError("boom")
sink = FlakySink()
buffer = MetricBuffer(sink, max_size=10)
buffer.record(MetricPoint(name="latency", value=42.0))
try:
buffer.flush()
except RuntimeError:
pass
# Point was re-queued and delivered on the retry.
assert buffer.flush() == 1
assert sink.calls == 2
This snippet shows a common building block in telemetry pipelines: a client-side buffer that accumulates metric points in memory and ships them to a remote sink in batches, flushing whenever the buffer reaches a size threshold OR a time interval elapses — whichever comes first. Batching amortizes the cost of network round-trips and reduces load on the sink, while the time trigger bounds how long a point can sit unsent, keeping data reasonably fresh even under low traffic.
In MetricPoint, a lightweight dataclass captures the shape of a single sample and a default_tags factory avoids the classic mutable-default-argument bug. It exposes as_dict so the sink layer decides on wire encoding rather than the buffer.
MetricBuffer is the core. record appends under a threading.Lock and returns whether the buffer is now full; the caller uses that signal to trigger an out-of-band flush without waiting for the timer. flush swaps the internal list for a fresh one while holding the lock (a cheap drain-and-release), then does the slow network call outside the lock so recording threads are never blocked on I/O. This drain-then-send ordering is the key concurrency trade-off: it keeps the critical section tiny at the cost of briefly holding a detached batch that must not be lost. To protect against that, _flush_locked re-queues the batch back onto the front of the buffer if the sink raises, giving simple at-least-once retry semantics; duplicates are accepted as the price of durability.
BatchingMetricsClient in the same tab wires timing to the buffer. A daemon Thread runs _run, sleeping on a threading.Event so close can wake it immediately for a clean shutdown. Each flush_interval it flushes; gauge and increment call record and eagerly flush when the size trigger fires. close sets _stop, joins the thread, and performs a final flush so buffered points are not dropped on exit.
HttpMetricSink shows a realistic sink: it posts the batch as JSON with a short timeout and raises on non-2xx so the buffer's retry path engages. The test_batching_flush tab demonstrates both triggers — filling to capacity forces an immediate send, and advancing the fake clock exercises the interval path — and asserts the sink received exactly one batch of the expected size.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
Share this code
Here's the card — post it anywhere.