Skip to content

Commit 6aa54da

Browse files
authored
streaming: generation-qualified stream lifecycle and exact publication (#82)
* streaming: generation-qualified stream lifecycle and exact publication Streams bind lazily to one Redis-owned generation with a distinct physical key per explicit recreation. Every metadata mutation - publication, event removal, consumer-group recovery, consumer registration, keep-alive refresh, stale recovery, acknowledgement, and destruction - verifies the exact generation atomically in Redis, so destroyed-generation metadata can never be resurrected by a concurrent sink. Adds idempotent AddOnce publication and side-effect-free Snapshot reads, Redis-owned TTL/deadline retention, and a fenced stale-recovery lease. Exported option structs keep their v1 fields as a stable ordered prefix; unkeyed literals are pinned internally and keyed v1 construction is pinned for external compatibility. * streaming: address review findings Delete the orphaned rollbackStreamRegistration helper (atomic registration made Redis rollback unnecessary), keep the exported Stream.MaxLen immutable after construction so concurrent readers never race with generation binding (the canonical bound lives in the private snapshot), and pin the v1 option fields to their leading positions with a reflect-based prefix check that keyed literals cannot provide. * pool: synchronize the shared test hasher in TestStaleNodeStreamCleanup The hasher closure is shared by two nodes and invoked from concurrent routing and rebalance goroutines; its job counter must be atomic. Latent on main, exposed by the streaming lifecycle's timing changes under -race. * streaming: gofmt
1 parent e91bdbb commit 6aa54da

37 files changed

Lines changed: 7021 additions & 2019 deletions

examples/rmap/basics/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func main() {
1414
ctx := context.Background()
1515

1616
// Create Redis client
17-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
17+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1818

1919
// Make sure Redis is up and running and we can connect to it
2020
if err := rdb.Ping(ctx).Err(); err != nil {

examples/rmap/multi-nodes/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func main() {
1818
ctx := context.Background()
1919

2020
// Create Redis client
21-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
21+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
2222

2323
// Make sure Redis is up and running and we can connect to it
2424
if err := rdb.Ping(ctx).Err(); err != nil {

examples/streaming/README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,34 @@ To run the examples, follow these steps:
1010

1111
2. Open a terminal or command prompt.
1212

13+
The examples read `REDIS_ADDR` and `REDIS_PASSWORD`; from the repository
14+
root, `source .env` selects the default local Redis configuration.
15+
1316
3. Clone the `goadesign/pulse` repository by running the following command:
14-
```
17+
```bash
1518
git clone https://github.com/goadesign/pulse.git
1619
```
1720

1821
4. Change into the example directory (e.g. `examples/streaming/single-reader`):
19-
```
22+
```bash
2023
cd pulse/examples/streaming/single-reader
2124
```
22-
```
2325

24-
5. Install the required dependencies by running the following command:
25-
```
26-
go get github.com/redis/go-redis/v9 goa.design/pulse/rmap
26+
5. Download the repository's pinned dependencies:
27+
```bash
28+
go mod download
2729
```
2830

2931
6. Build the Go program by executing the following command:
30-
```
32+
```bash
3133
go build
3234
```
3335

3436
7. Run the program using the following command:
35-
```
37+
```bash
3638
./single-reader
3739
```
3840

3941
This will execute the program and demonstrate the basic operations on streaming.
42+
The [`exact-publication`](exact-publication/main.go) example shows
43+
deadline-owned `AddOnce` retries and read-only snapshots.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"github.com/redis/go-redis/v9"
10+
11+
"goa.design/pulse/streaming"
12+
"goa.design/pulse/streaming/options"
13+
)
14+
15+
func main() {
16+
ctx := context.Background()
17+
rdb := redis.NewClient(&redis.Options{
18+
Addr: os.Getenv("REDIS_ADDR"),
19+
Password: os.Getenv("REDIS_PASSWORD"),
20+
})
21+
if err := rdb.Ping(ctx).Err(); err != nil {
22+
panic(err)
23+
}
24+
25+
deadline := time.Now().Add(time.Minute).Truncate(time.Millisecond)
26+
stream, err := streaming.NewStream(
27+
"exact-publication",
28+
rdb,
29+
options.WithStreamDeadline(deadline),
30+
options.WithStreamMaxLen(1_000),
31+
)
32+
if err != nil {
33+
panic(err)
34+
}
35+
defer func() {
36+
if err := stream.Destroy(ctx); err != nil {
37+
panic(err)
38+
}
39+
}()
40+
41+
first, err := stream.AddOnce(
42+
ctx,
43+
"facility-42:alarm-7",
44+
"alarm-opened",
45+
[]byte("high discharge pressure"),
46+
options.WithTopic("alarms"),
47+
)
48+
if err != nil {
49+
panic(err)
50+
}
51+
retry, err := stream.AddOnce(
52+
ctx,
53+
"facility-42:alarm-7",
54+
"alarm-opened",
55+
[]byte("high discharge pressure"),
56+
options.WithTopic("alarms"),
57+
)
58+
if err != nil {
59+
panic(err)
60+
}
61+
fmt.Printf("first=%s retry=%s\n", first, retry)
62+
63+
events, err := stream.Snapshot(ctx)
64+
if err != nil {
65+
panic(err)
66+
}
67+
for _, event := range events {
68+
fmt.Printf("%s %s: %s\n", event.ID(), event.EventName(), event.Payload())
69+
}
70+
}

examples/streaming/multi-readers/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func main() {
1515
// Create Redis client
16-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
16+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1717
ctx := context.Background()
1818

1919
// Make sure Redis is up and running and we can connect to it

examples/streaming/multi-sinks/main.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// NOTE: the example below does not handle errors for brevity.
1515
func main() {
1616
// Create Redis client
17-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
17+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1818
ctx := context.Background()
1919

2020
// Make sure Redis is up and running and we can connect to it
@@ -29,7 +29,11 @@ func main() {
2929
}
3030

3131
// Don't forget to destroy the stream when done
32-
defer stream.Destroy(ctx)
32+
defer func() {
33+
if err := stream.Destroy(ctx); err != nil {
34+
panic(err)
35+
}
36+
}()
3337

3438
// Write 2 events to the stream
3539
id1, err := stream.Add(ctx, "event 1", []byte("payload 1"))
@@ -54,7 +58,11 @@ func main() {
5458
}
5559

5660
// Don't forget to close the sink when done
57-
defer sink1.Close(ctx)
61+
defer func() {
62+
if err := sink1.Close(ctx); err != nil {
63+
panic(err)
64+
}
65+
}()
5866

5967
// Read and acknowlege event
6068
ev := <-sink1.Subscribe()
@@ -70,12 +78,16 @@ func main() {
7078
if err != nil {
7179
panic(err)
7280
}
73-
defer sink2.Close(ctx)
81+
defer func() {
82+
if err := sink2.Close(ctx); err != nil {
83+
panic(err)
84+
}
85+
}()
7486

7587
// Read second event
7688
ev = <-sink2.Subscribe()
7789
fmt.Printf("sink 2, event: %s, payload: %s\n", ev.EventName, ev.Payload)
78-
if sink2.Ack(ctx, ev); err != nil {
90+
if err := sink2.Ack(ctx, ev); err != nil {
7991
panic(err)
8092
}
8193
}

examples/streaming/multi-streams/main.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// Note: the example below does not handle errors for brevity.
1515
func main() {
1616
// Create Redis client
17-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
17+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1818
ctx := context.Background()
1919

2020
// Make sure Redis is up and running and we can connect to it
@@ -29,7 +29,11 @@ func main() {
2929
}
3030

3131
// Don't forget to destroy the stream when done
32-
defer stream1.Destroy(ctx)
32+
defer func() {
33+
if err := stream1.Destroy(ctx); err != nil {
34+
panic(err)
35+
}
36+
}()
3337

3438
// Create sink
3539
sink, err := stream1.NewSink(ctx, "multistreams-sink",
@@ -40,7 +44,11 @@ func main() {
4044
}
4145

4246
// Don't forget to close the sink when done
43-
defer sink.Close(ctx)
47+
defer func() {
48+
if err := sink.Close(ctx); err != nil {
49+
panic(err)
50+
}
51+
}()
4452

4553
// Subscribe to events
4654
c := sink.Subscribe()
@@ -57,7 +65,11 @@ func main() {
5765
if err != nil {
5866
panic(err)
5967
}
60-
defer stream2.Destroy(ctx)
68+
defer func() {
69+
if err := stream2.Destroy(ctx); err != nil {
70+
panic(err)
71+
}
72+
}()
6173

6274
// Add stream to sink
6375
err = sink.AddStream(ctx, stream2)

examples/streaming/pub-sub/main.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// NOTE: the example below does not handle errors for brevity.
1515
func main() {
1616
// Create Redis client
17-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
17+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1818
ctx := context.Background()
1919
if err := rdb.Ping(ctx).Err(); err != nil {
2020
panic(err)
@@ -27,7 +27,11 @@ func main() {
2727
}
2828

2929
// Don't forget to destroy the stream when done
30-
defer stream.Destroy(ctx)
30+
defer func() {
31+
if err := stream.Destroy(ctx); err != nil {
32+
panic(err)
33+
}
34+
}()
3135

3236
// Add a new event to topic "my-topic"
3337
id1, err := stream.Add(ctx,
@@ -56,7 +60,11 @@ func main() {
5660
}
5761

5862
// Don't forget to close the sink when done
59-
defer sink.Close(ctx)
63+
defer func() {
64+
if err := sink.Close(ctx); err != nil {
65+
panic(err)
66+
}
67+
}()
6068

6169
// Read both events
6270
c := sink.Subscribe()

examples/streaming/single-reader/main.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func main() {
1515
// Create Redis client
16-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
16+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1717
ctx := context.Background()
1818
if err := rdb.Ping(ctx).Err(); err != nil {
1919
panic(err)
@@ -26,7 +26,11 @@ func main() {
2626
}
2727

2828
// Don't forget to destroy the stream when done
29-
defer stream.Destroy(ctx)
29+
defer func() {
30+
if err := stream.Destroy(ctx); err != nil {
31+
panic(err)
32+
}
33+
}()
3034

3135
// Add a new event
3236
id, err := stream.Add(ctx, "event", []byte("payload"))

examples/streaming/single-sink/main.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func main() {
1515
// Create Redis client
16-
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: os.Getenv("REDIS_PASSWORD")})
16+
rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR"), Password: os.Getenv("REDIS_PASSWORD")})
1717
ctx := context.Background()
1818
if err := rdb.Ping(ctx).Err(); err != nil {
1919
panic(err)
@@ -26,7 +26,11 @@ func main() {
2626
}
2727

2828
// Don't forget to destroy the stream when done
29-
defer stream.Destroy(ctx)
29+
defer func() {
30+
if err := stream.Destroy(ctx); err != nil {
31+
panic(err)
32+
}
33+
}()
3034

3135
// Add a new event
3236
id, err := stream.Add(ctx, "event", []byte("payload"))
@@ -45,7 +49,11 @@ func main() {
4549
}
4650

4751
// Don't forget to close the sink when done
48-
defer sink.Close(ctx)
52+
defer func() {
53+
if err := sink.Close(ctx); err != nil {
54+
panic(err)
55+
}
56+
}()
4957

5058
// Consume event
5159
ev := <-sink.Subscribe()

0 commit comments

Comments
 (0)