Skip to content
JinchenyuanPublic

About

A lightweight microservice write by Go!

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Project Overview

This is a microservices project based on go-micro, It is suitable for lightweight mobile apps or small game backends.

Tech Stack

Framework

  • API gateway / HTTP handler: Gin
  • Microservices framework: go-micro

Drivers

  • Database: pgx (PostgreSQL driver)
  • Cache: go-redis (Redis client)

Middleware & Components

  • Cache: Redis
  • Database: PostgreSQL
  • Service discovery & configuration: etcd

Reminder MVP

The framework now includes a minimal reminder worker built on PostgreSQL and the Mesa lifecycle.

Included capabilities

  • Store reminder records in PostgreSQL
  • Poll due reminders in the background
  • Deliver reminders through a user-supplied notifier
  • Retry failed deliveries with backoff
  • Recover stale processing reminders after restart
  • Cancel or reschedule reminders by key

Basic usage

mesa, err := wego.New(
	wego.WithDSN(dsn),
)
if err != nil {
	panic(err)
}

svc := reminder.NewService(
	mesa.DB,
	reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
		// send email, push, webhook, etc.
		return nil
	}),
)

// Run versioned migrations before starting Mesa (normally in deployment tooling).
if err := reminder.Migrate(context.Background(), mesa.DB); err != nil {
	panic(err)
}

if err := mesa.RegisterComponent(svc); err != nil {
	panic(err)
}

reminderSvc, ok := wego.GetComponentAs[*reminder.Service](mesa, svc.Name())
if !ok {
	panic("reminder component not found")
}

_, err := reminderSvc.Create(context.Background(), reminder.CreateParams{
	Key:        "order-123-pay-deadline",
	UserID:     "42",
	Channel:    "in_app",
	Payload:    `{"title":"Payment due soon"}`,
	ScheduleAt: time.Now().Add(10 * time.Minute),
})

Querying and state errors

Reminder schema changes are explicit: call reminder.Migrate(ctx, db) before the worker starts. Service.Start never creates or alters tables.

item, err := reminderSvc.GetByKey(ctx, "order-123-pay-deadline")
if errors.Is(err, reminder.ErrNotFound) {
	// handle a missing reminder
}

page, err := reminderSvc.List(ctx, reminder.ListParams{
	UserID:   "42",
	Statuses: []reminder.Status{reminder.StatusPending},
	Limit:    50,
})
nextPage, err := reminderSvc.List(ctx, reminder.ListParams{
	UserID: "42",
	Limit:  50,
	Cursor: page.NextCursor,
})

_ = item
_ = nextPage

Create returns ErrAlreadyExists for duplicate keys. Cancel and reschedule return ErrNotFound for unknown keys and ErrStateConflict when the current state does not allow the operation. Canceling an already canceled reminder succeeds.

Dispatcher notifier

When reminder scenes grow, you can register handlers by channel, by payload type, or by an exact channel + type route.

dispatcher := reminder.NewDispatcherNotifier()

_ = dispatcher.RegisterChannel("in_app", reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
	// generic in-app delivery
	return nil
}))

_ = dispatcher.RegisterType("payment_due", reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
	// shared payment_due template logic
	return nil
}))

_ = dispatcher.Register("sms", "payment_due", reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
	// exact route for sms + payment_due
	return nil
}))

_ = dispatcher.SetFallback(reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
	// last-resort handler
	return nil
}))

mesa, err := wego.New(
	wego.WithDSN(dsn),
)
if err != nil {
	panic(err)
}

svc := reminder.NewService(mesa.DB, dispatcher)

if err := mesa.RegisterComponent(svc); err != nil {
	panic(err)
}

component, ok := mesa.GetComponent(svc.Name())
if !ok {
	panic("component not found")
}

reminderSvc := component.(*reminder.Service)

_, _ = reminderSvc.Create(ctx, reminder.CreateParams{
	Key:        "order-123-pay-deadline",
	UserID:     "42",
	Channel:    "sms",
	Payload:    `{"type":"payment_due","title":"Payment due soon"}`,
	ScheduleAt: time.Now().Add(10 * time.Minute),
})

Dispatch priority is:

  • exact channel + type
  • payload type
  • channel
  • fallback

Component registry

Mesa now acts as a generic component registry.

if err := mesa.RegisterComponent(svc); err != nil {
	panic(err)
}

component, ok := mesa.GetComponent("reminder")
if !ok {
	panic("component not found")
}

reminderSvc, ok := wego.GetComponentAs[*reminder.Service](mesa, "reminder")
if !ok {
	panic("unexpected component type")
}

_ = component
_ = reminderSvc

Default component configuration

WithComponents(...) lets Mesa auto-register built-in components in one place.

mesa, err := wego.New(
	wego.WithDSN(dsn),
	wego.WithRedisConfig(wego.RedisConfig{
		Addr: "127.0.0.1:6379",
	}),
	wego.WithComponents(wego.ComponentsConfig{
		Reminder: wego.ReminderComponentConfig{
			Enabled:  true,
			Notifier: reminder.NotifierFunc(func(ctx context.Context, r *reminder.Reminder) error {
				return nil
			}),
			PollInterval: 2 * time.Second,
		},
		PubSub: wego.PubSubComponentConfig{
			Enabled:      true,
			StreamPrefix: "wego:events",
		},
	}),
)
if err != nil {
	panic(err)
}

reminderSvc := mesa.MustGetReminder()
pubsubSvc := mesa.MustGetPubSub()

_ = reminderSvc
_ = pubsubSvc

Pub/Sub component

pubsub provides a Redis Streams based component for cross-instance publish and subscribe with consumer groups.

Included capabilities

  • Publish messages to a topic backed by Redis Streams
  • Register subscriptions before Mesa runtime starts
  • Competing consumers within one consumer group
  • Fan-out delivery through different consumer groups
  • Retry failed deliveries with configurable backoff
  • Ack on success and dead-letter on max delivery attempts

Basic usage

mesa, err := wego.New(
	wego.WithDSN(dsn),
	wego.WithRedisConfig(wego.RedisConfig{
		Addr: "127.0.0.1:6379",
	}),
	wego.WithComponents(wego.ComponentsConfig{
		PubSub: wego.PubSubComponentConfig{
			Enabled: true,
		},
	}),
)
if err != nil {
	panic(err)
}

pubsubSvc := mesa.MustGetPubSub()

pubsubSvc.MustSubscribe(pubsub.Subscription{
	Topic: "order.created",
	Group: "billing",
	Handler: pubsub.HandlerFunc(func(ctx context.Context, delivery *pubsub.Delivery) error {
		fmt.Println("received:", delivery.Message.Type, string(delivery.Message.Data))
		return nil
	}),
})

_, err := pubsubSvc.Publish(context.Background(), "order.created", pubsub.Message{
	Key:  "order-123",
	Type: "order.created",
	Data: []byte(`{"id":"123"}`),
})
if err != nil {
	panic(err)
}

Runtime rules are:

  • subscriptions must be registered before mesa.Run(ctx)
  • handlers must be idempotent because delivery is at least once
  • one topic + group maps to one handler within a process
  • use different consumer groups when the same topic needs fan-out
  • handler failures are re-published through an internal retry queue using the configured backoff policy
  • retry queues are isolated by topic + group; scheduling a retry and acknowledging the source entry are atomic on Redis
  • auto-registration only happens when wego.WithComponents(...) or wego.WithPubSub(...) is provided and Redis is configured

Pub/Sub can also be configured with framework-level fields such as Name, RetryPolicy, StreamPrefix, StreamMaxLen, StreamBlock, StreamReadCount, and dead-letter options through wego.PubSubComponentConfig.

Pub/Sub integration tests

The Redis-backed integration tests for pubsub are opt-in and use the integration build tag.

Required environment variables:

  • WEGO_REDIS_TEST_ADDR: Redis address such as 127.0.0.1:6379

Optional environment variables:

  • WEGO_REDIS_TEST_PASSWORD: Redis password
  • WEGO_REDIS_TEST_DB: Redis database index, defaults to 15

Run the default unit tests:

go test ./pubsub

Run the Redis integration tests:

WEGO_REDIS_TEST_ADDR=127.0.0.1:6379 go test -tags=integration ./pubsub

Run the full repository test suite without integration tests:

go test ./...

If WEGO_REDIS_TEST_ADDR is not set or Redis is unreachable, the integration tests are skipped.

Reminder integration tests

PostgreSQL-backed Reminder tests are opt-in. Each test creates and removes an isolated schema and applies the production migration.

WEGO_POSTGRES_TEST_DSN='postgres://user:password@127.0.0.1:5432/land_contract?sslmode=disable' go test -tags=integration ./reminder

TCP transport

transport/tcp provides a generic TCP server implementation that matches the existing transport.Server lifecycle.

tcpServer := tcp.NewTCPServer(
	tcp.WithHost(net.ParseIP("0.0.0.0")),
	tcp.WithPort(9000),
	tcp.WithHandler(func(ctx context.Context, conn net.Conn) {
		defer conn.Close()

		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			return
		}

		_, _ = conn.Write(buf[:n])
	}),
)

mesa, err := wego.New(
	wego.WithDSN(dsn),
	wego.WithServers(tcpServer),
)
if err != nil {
	panic(err)
}

Installation (main)

go get github.com/Jinchenyuan/wego@main

Runtime and health checks

New only initializes configured dependencies and returns connection or configuration errors. Run accepts a context, and Shutdown is safe to call more than once.

When the default HTTP server is enabled with WithHttpPort, it exposes GET /livez and GET /readyz. The same status is available through mesa.Liveness() and mesa.Readiness(ctx).

mesa, err := wego.New(wego.WithHttpPort(8080))
if err != nil {
	return err
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
return mesa.Run(ctx)

Validated configuration

wego.Config is the typed production configuration surface. Applications remain responsible for loading environment variables, files, and secrets, then call Validate or Options before creating Mesa:

cfg := wego.Config{
	Profile: wego.Profile{Name: "orders"},
	HTTP:    wego.HTTPConfig{Port: 8080},
	Redis:   wego.RedisConfig{Addr: "127.0.0.1:6379"},
}
opts, err := cfg.Options()
if err != nil {
	return err
}
mesa, err := wego.New(opts...)

Configuration errors include the invalid field path and may be inspected with errors.As(err, *wego.ConfigError).

Pluggable authentication

middleware.Authenticate accepts a context-aware Authenticator. Successful authentication stores a Principal in the request context for handlers to read with middleware.PrincipalFromContext. The existing AuthMiddleware remains available for header ID plus cached-token schemes.

Production baseline

The default HTTP transport uses bounded header, request, response, idle, body, and header sizes. Override them without breaking the legacy WithHttpPort API:

m, err := wego.New(wego.WithHTTPConfig(wego.HTTPConfig{
	Port:              8080,
	ReadHeaderTimeout: 3 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      20 * time.Second,
	IdleTimeout:       60 * time.Second,
	MaxHeaderBytes:    1 << 20,
	MaxBodyBytes:      8 << 20,
	RequestTimeout:    15 * time.Second,
}))

/livez, /readyz, and /metrics are exposed by the default server. Logs are newline-delimited JSON. Redis TLS can be configured with RedisConfig.TLSConfig; TLS termination for public HTTP traffic is expected at the Kubernetes Ingress. Reusable CORS, security-header, and per-instance rate limit middleware is available in middleware.

OpenTelemetry tracing

Tracing is configured per Mesa instance and is disabled by default. When enabled, Wego exports spans through OTLP gRPC and automatically instruments the HTTP server, go-micro handlers/clients, PostgreSQL, Redis, Pub/Sub, and Reminder delivery. W3C trace context is propagated through HTTP headers and Pub/Sub message headers.

cfg := wego.Config{
	Profile: wego.Profile{Name: "orders"},
	Telemetry: telemetry.Config{
		Enabled:          true,
		OTLPEndpoint:     "otel-collector:4317",
		Insecure:         true, // use false with TLS credentials at the collector
		TraceSampleRatio: 0.1,
	},
}
opts, err := cfg.Options()
if err != nil {
	return err
}
mesa, err := wego.New(opts...)

The default /metrics endpoint remains available and includes bounded-label request, database, Redis, Pub/Sub, and Reminder counters and duration summaries. Telemetry never records request bodies, message payloads, credentials, user IDs, or reminder keys.

Deployable HTTP example

examples/http-server is a minimal application that uses the Mesa lifecycle, registers GET /hello, and includes Docker and Helm deployment assets. Run it locally from the repository root:

go run ./examples/http-server
curl http://127.0.0.1:8080/hello

The default server also exposes /livez, /readyz, and /metrics. Override the port with WEGO_HTTP_PORT.

Build the image with the repository root as the Docker context:

docker build -f examples/http-server/Dockerfile -t wego-http-server-example .
docker run --rm -p 8080:8080 wego-http-server-example

Install the example chart after publishing the image to a registry:

helm upgrade --install wego-http-server \
  examples/http-server/deploy/helm/http-server \
  --set image.repository=registry.example.com/wego-http-server-example \
  --set image.tag=1.0.0

Usage

Import packages to your .go files.

"github.com/Jinchenyuan/wego"
"github.com/Jinchenyuan/wego/logger"
"github.com/Jinchenyuan/wego/middleware"
"github.com/Jinchenyuan/wego/transport"
"github.com/Jinchenyuan/wego/transport/micro"

Run a mesa instance with your configuration.

// read config from file
cfg, err := config.Read("config.toml")
if err != nil {
	fmt.Printf("failed to read config: %v\n", err)
	return
}
m, err := wego.New(
	wego.WithEtcdConfig(clientv3.Config{
		Endpoints:   cfg.Etcd.Endpoints,
		DialTimeout: 5 * time.Second,
		Username:    cfg.Etcd.User,
		Password:    cfg.Etcd.Password,
	}),
	wego.WithHttpPort(cfg.Http.Port),
	wego.WithDSN(cfg.PostgreSQL.DSN),
	wego.WithLogLevel(logger.ParseLevel(cfg.Log.Level)),
	wego.WithRedisConfig(wego.RedisConfig{
		Addr:     cfg.Redis.Addr,
		Password: cfg.Redis.Password,
		DB:       cfg.Redis.DB,
	}),
	wego.WithProfile(wego.Profile{
		Name: cfg.Profile.Name,
	}),
)
if err != nil {
	fmt.Printf("failed to initialize mesa: %v\n", err)
	return
}

ginhandler.SetAuthMiddleware(middleware.AuthMiddleware("account", func(id string) string {
	cacheToken, err := m.Redis.Get(context.Background(), fmt.Sprintf("token:%s", id)).Result()
	if err != nil {
		return ""
	}
	return cacheToken
}, cfg.Http.ExcludeAuthPaths...))

ginhandler.Registry()

ms := m.GetServerByType(transport.MICRO_SERVER).(*micro.Service)
ms.NewServiceClients(serviceclient.Registry)

if err := m.Run(context.Background()); err != nil {
	fmt.Printf("failed to run mesa: %v\n", err)
}

example project see: https://github.com/Jinchenyuan/weserver

About

A lightweight microservice write by Go!

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

⌂ 换网址