Skip to content

Pub/Sub examples

A tour of redis publish/subscribe — publishing, the handler-based subscriber, raw subscriptions, and the three jobs fan-out with no acknowledgement is genuinely the right tool for.

Read Pub/Sub first for the concepts — these files show them wired together.

sh
git clone https://gitlab.finema.co/finema/idin-core.git
cd idin-core/v2/examples/pubsub
go run .

Both ends

Publishing something worth publishing, and the two ways to receive it.

What it is actually for

The shapes that fit, and the line where pub/sub stops being the right answer.

Putting it together

main.go starts a subscriber and a websocket hub in one process and publishes into both. With no CACHE_* configuration it falls back to the memory cache, which carries an in-process broker — so go run . delivers messages with no redis running.

go
// Command pubsub is a runnable tour of the v2 redis publish/subscribe support:
// publishing, the handler-based subscriber, raw subscriptions, and the three
// jobs pub/sub is genuinely the right tool for.
//
// Each example lives in its own file:
//
//	01_publish.go     publishing, payloads, channel names, prefixes
//	02_subscriber.go  NewSubscriber, handlers, options, lifecycle, raw channels
//	03_patterns.go    invalidation, config reloads, websockets — and the limits
//
// Run it with: go run ./examples/pubsub
//
// It runs without redis: with no CACHE_* configuration it falls back to the
// memory cache, which carries an in-process broker, so every example below
// actually delivers. For a real redis:
//
//	docker run -d --rm -p 6379:6379 redis:7-alpine
//
// then point the example at it:
//
//	APP_CACHE_HOST=127.0.0.1:6379 go run ./examples/pubsub
package main

import (
	"context"
	"os"
	"os/signal"
	"syscall"
	"time"

	core "gitlab.finema.co/finema/idin-core/v2"
)

func main() {
	env, err := core.NewEnv()
	if err != nil {
		panic(err)
	}

	// NewCache pings before returning, so a wrong address fails here rather than
	// on the first request. The memory cache is the fallback because a subscriber
	// with no cache at all cannot start: subscribing fails with PUBSUB_DISABLED
	// on purpose, since a subscriber that silently receives nothing forever is a
	// service that looks healthy while doing none of its work.
	cache, cacheErr := core.NewCache(env)
	if cacheErr != nil {
		cache = core.NewMemoryCache()
	}

	app, err := core.NewApp(env, core.WithCache("default", cache))
	if err != nil {
		panic(err)
	}
	if cacheErr != nil {
		app.Log().Warn("no redis: running on the in-process memory broker",
			"err", cacheErr,
			"hint", "docker run -d --rm -p 6379:6379 redis:7-alpine")
		// What the memory broker does not prove: fan-out to *other replicas*, the
		// disconnection of a slow subscriber, and reconnection after a redis
		// restart are all real-redis behaviour.
	}

	sub := newSubscriber(app)
	if err := sub.Start(); err != nil {
		panic(err)
	}

	// The websocket hub is a raw subscription with its own lifetime — one per
	// process, fanned out in memory.
	h := newHub()
	go func() {
		if err := h.Run(app); err != nil {
			app.Log().Error("hub stopped", "err", err)
		}
	}()

	demoPublish(app)

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	app.Log().Info("pubsub example running — press ctrl-c to stop")
	<-ctx.Done()

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	// Shutdown stops every subscriber the App handed out before closing the
	// connections they are reading from, waiting for in-flight handlers within
	// this deadline.
	_ = app.Shutdown(shutdownCtx)
}

// demoPublish shows the one ordering rule that always applies: subscribe first,
// then publish. There is no buffering and no replay, so a message published
// before Start is simply gone.
func demoPublish(app *core.App) {
	ctx := app.NewContext(context.Background())

	ctx.PubSub().Publish("user.updated", "u-1")
	ctx.PubSub().Publish("order.created", "o-1")
	ctx.PubSub().Publish("notify.u-1", map[string]string{"text": "Your export is ready"})

	// The wire name is what a subscriber outside this framework has to use.
	app.Log().Info("channel names on the wire",
		"logical", "user.updated",
		"wire", ctx.PubSub().Channel("user.updated"))
}

Maintained by Passakon Puttasuwan & Dev Core Team.