Skip to content

Commit 8f2fa1b

Browse files
committed
Initial implementation
0 parents  commit 8f2fa1b

23 files changed

Lines changed: 2049 additions & 0 deletions

.github/workflows/test.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: test
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v6
12+
- uses: actions/setup-go@v6
13+
with:
14+
go-version: '1.26'
15+
- uses: golangci/golangci-lint-action@v9
16+
with:
17+
version: v2.12.2
18+
- run: go test ./...
19+
- run: go test -race ./...
20+
- run: go test -run='^$' -bench=. -benchtime=100ms ./...

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
.idea/
3+
.vscode/
4+
*.test
5+
coverage.out
6+
dist/

.golangci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
version: "2"
2+
run:
3+
modules-download-mode: readonly
4+
tests: true
5+
linters:
6+
default: none
7+
enable:
8+
- errcheck
9+
- govet
10+
- ineffassign
11+
- staticcheck
12+
- unused
13+
exclusions:
14+
generated: lax
15+
presets:
16+
- comments
17+
- common-false-positives
18+
- legacy
19+
- std-error-handling
20+
paths:
21+
- third_party$
22+
- builtin$
23+
- examples$
24+
issues:
25+
max-issues-per-linter: 0
26+
max-same-issues: 0
27+
formatters:
28+
exclusions:
29+
generated: lax
30+
paths:
31+
- third_party$
32+
- builtin$
33+
- examples$

API.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Public API
2+
3+
```go
4+
package chanprobe
5+
6+
var (
7+
ErrClosed error
8+
ErrFull error
9+
)
10+
11+
type DropPolicy int
12+
13+
const (
14+
Block DropPolicy = iota
15+
DropNewest
16+
DropOldest
17+
)
18+
19+
type Option func(*Options)
20+
21+
type Options struct {
22+
DropPolicy DropPolicy
23+
Registry *Registry
24+
}
25+
26+
func WithDropPolicy(policy DropPolicy) Option
27+
func WithRegistry(reg *Registry) Option
28+
29+
type Queue[T any] struct{}
30+
31+
func New[T any](name string, capacity int, opts ...Option) *Queue[T]
32+
33+
func (q *Queue[T]) Name() string
34+
func (q *Queue[T]) Send(ctx context.Context, item T) error
35+
func (q *Queue[T]) Recv(ctx context.Context) (T, bool)
36+
func (q *Queue[T]) TrySend(item T) bool
37+
func (q *Queue[T]) TryRecv() (T, bool)
38+
func (q *Queue[T]) Close()
39+
func (q *Queue[T]) Len() int
40+
func (q *Queue[T]) Cap() int
41+
func (q *Queue[T]) Snapshot() Snapshot
42+
43+
type Snapshot struct {
44+
Name string `json:"name"`
45+
Len int `json:"len"`
46+
Cap int `json:"cap"`
47+
Closed bool `json:"closed"`
48+
SentTotal uint64 `json:"sent_total"`
49+
ReceivedTotal uint64 `json:"received_total"`
50+
DroppedTotal uint64 `json:"dropped_total"`
51+
SendBlockedTotal uint64 `json:"send_blocked_total"`
52+
RecvBlockedTotal uint64 `json:"recv_blocked_total"`
53+
SendWaitTotal time.Duration `json:"send_wait_total"`
54+
RecvWaitTotal time.Duration `json:"recv_wait_total"`
55+
ItemWaitTotal time.Duration `json:"item_wait_total"`
56+
OldestItemAge time.Duration `json:"oldest_item_age"`
57+
}
58+
59+
type Snapshoter interface {
60+
Snapshot() Snapshot
61+
}
62+
63+
type Registry struct{}
64+
65+
func NewRegistry() *Registry
66+
func DefaultRegistry() *Registry
67+
func (r *Registry) Register(name string, snapper Snapshoter)
68+
func (r *Registry) Unregister(name string)
69+
func (r *Registry) Snapshots() []Snapshot
70+
71+
func PublishExpvar(name string, reg *Registry)
72+
```
73+
74+
## Notes
75+
76+
- `New` panics for an empty name or non-positive capacity.
77+
- `Send` returns `ErrClosed` after `Close`.
78+
- `Send` returns `ctx.Err()` when a blocking send is canceled.
79+
- `Recv` returns `ok=false` when the queue is closed and drained, or when a
80+
blocking receive is canceled.
81+
- `PublishExpvar` uses `DefaultRegistry()` when the registry argument is nil.

BENCHMARKS.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Benchmark plan
2+
3+
Benchmarks should be honest and not overclaim.
4+
5+
Run:
6+
7+
```bash
8+
go test -bench=. -benchmem ./...
9+
```
10+
11+
For more stable local numbers, run multiple samples:
12+
13+
```bash
14+
go test -run='^$' -bench=. -benchmem -count=5 ./...
15+
```
16+
17+
## Benchmark cases
18+
19+
1. Plain channel send/recv same goroutine.
20+
2. `chanprobe.Queue` Send/Recv same goroutine.
21+
3. Plain channel producer/consumer.
22+
4. `chanprobe.Queue` producer/consumer.
23+
5. TrySend/TryRecv.
24+
6. Snapshot under load.
25+
26+
## Interpreting results
27+
28+
Compare related pairs:
29+
30+
- `BenchmarkChannelSameGoroutine` vs `BenchmarkQueueSameGoroutine`
31+
- `BenchmarkChannelProducerConsumer` vs `BenchmarkQueueProducerConsumer`
32+
33+
Native channels are the baseline. `chanprobe.Queue` is expected to cost more
34+
because it tracks counters, wait durations, item age, close state, and supports
35+
context-aware blocking operations.
36+
37+
`BenchmarkQueueTrySendTryRecv` measures the non-blocking fast path.
38+
39+
`BenchmarkQueueSnapshot` measures the cost of reading observability data from a
40+
populated queue.
41+
42+
Example local result on an AMD Ryzen 3 4300U:
43+
44+
```text
45+
BenchmarkChannelSameGoroutine-4 30.80 ns/op 0 B/op 0 allocs/op
46+
BenchmarkQueueSameGoroutine-4 2293 ns/op 0 B/op 0 allocs/op
47+
BenchmarkChannelProducerConsumer-4 51.11 ns/op 0 B/op 0 allocs/op
48+
BenchmarkQueueProducerConsumer-4 2389 ns/op 0 B/op 0 allocs/op
49+
BenchmarkQueueTrySendTryRecv-4 2262 ns/op 0 B/op 0 allocs/op
50+
BenchmarkQueueSnapshot-4 1164 ns/op 0 B/op 0 allocs/op
51+
```
52+
53+
Treat these as local reference numbers, not a universal performance claim.
54+
55+
## README benchmark wording
56+
57+
Do not claim `chanprobe` is faster than channels.
58+
59+
Expected positioning:
60+
61+
> `chanprobe` adds observability and context-aware queue operations. It has overhead compared to native channels. Use it at important async boundaries where visibility matters.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 DevFlex Pro
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

METRICS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Metrics design
2+
3+
## Core principle
4+
5+
The core package exposes snapshots. Exporters convert snapshots to external systems.
6+
7+
This avoids forcing dependencies on users.
8+
9+
## Snapshot fields
10+
11+
### Len and Cap
12+
13+
Current queue occupancy.
14+
15+
Useful to detect saturation but not enough alone.
16+
17+
### SentTotal and ReceivedTotal
18+
19+
Basic throughput counters.
20+
21+
### DroppedTotal
22+
23+
Critical for lossy queues.
24+
25+
Increment when:
26+
27+
- `DropNewest` rejects incoming work.
28+
- `DropOldest` evicts old work.
29+
30+
### SendBlockedTotal
31+
32+
Increment when a `Send` call found a full queue and had to wait under `Block`.
33+
34+
### RecvBlockedTotal
35+
36+
Increment when a `Recv` call found an empty queue and had to wait.
37+
38+
### SendWaitTotal and RecvWaitTotal
39+
40+
Total accumulated waiting time.
41+
42+
Average wait can be derived as:
43+
44+
```text
45+
avg_send_wait = SendWaitTotal / SendBlockedTotal
46+
avg_recv_wait = RecvWaitTotal / RecvBlockedTotal
47+
```
48+
49+
### ItemWaitTotal
50+
51+
Total time items spent inside the queue before being received.
52+
53+
Average queue age can be derived as:
54+
55+
```text
56+
avg_item_wait = ItemWaitTotal / ReceivedTotal
57+
```
58+
59+
### OldestItemAge
60+
61+
Age of the oldest item currently in the queue.
62+
63+
This is one of the most useful incident metrics.
64+
65+
## v1 deliberately avoids histograms
66+
67+
Histograms require either:
68+
69+
- a dependency on a metrics backend,
70+
- a custom approximate histogram implementation,
71+
- or more API surface.
72+
73+
For v1, snapshots are enough. Prometheus/OpenTelemetry adapters can add histograms later.

0 commit comments

Comments
 (0)