项目文件夹

文件
Asim Aslam c7657f73f4
goreleaser / goreleaser (push) Has been cancelled
Refactor agent plan storage, update docs, and release v6 (#2977)
* test(harness): read agent plan from the scoped store

The store-scoping change moved an agent's plan from the default table
key agent/{name}/plan to its own table (database "agent", table {name},
key "plan"). The plan-delegate harness tests still read the old key and
failed with 'not found'; read through store.Scope(mem, "agent", name)
like the agent does.

* docs: orient agents-first across README, landing, and docs overview

Lead with agents (then services and flows), surface MCP + A2A as the
interop story, and frame agents as services. Landing hero and feature
grid reordered agents-first with an A2A gateway card.

* v6: module path go-micro.dev/v6, TLS secure by default, NewService

Cut v6. Three breaking changes, bundled so the major bump is paid once:

- Module path go-micro.dev/v5 -> go-micro.dev/v6 across all imports + go.mod.
- TLS verification on by default (was off). MICRO_TLS_SECURE removed;
  MICRO_TLS_INSECURE=true opts out for self-signed/dev.
- micro.NewService(name, opts...) is the canonical service constructor,
  symmetric with NewAgent/NewFlow; micro.New kept as a deprecated alias;
  the old name-less NewService(opts...) removed. Generators emit NewService.

Also ports the JWT auth token provider in-module (go-micro.dev/v6/auth/jwt/token
on golang-jwt/jwt/v5), dropping the v5-pinned github.com/micro/plugins/v5/auth/jwt
and the deprecated dgrijalva/jwt-go.

Docs/README/landing updated to v6 and @latest; v5->v6 migration guide added;
CHANGELOG cut as [6.0.0]. Blog posts left at their historical versions.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-18 11:55:35 +01:00

288 行
6.6 KiB
Go

// Package natsjs provides a NATS Jetstream implementation of the events.Stream interface.
package natsjs
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/google/uuid"
nats "github.com/nats-io/nats.go"
"github.com/pkg/errors"
"go-micro.dev/v6/events"
"go-micro.dev/v6/logger"
)
const (
defaultClusterID = "micro"
)
// NewStream returns an initialized nats stream or an error if the connection to the nats
// server could not be established.
func NewStream(opts ...Option) (events.Stream, error) {
// parse the options
options := Options{
ClientID: uuid.New().String(),
ClusterID: defaultClusterID,
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
s := &stream{opts: options}
conn, natsJetStreamCtx, err := connectToNatsJetStream(options)
if err != nil {
return nil, fmt.Errorf("error connecting to nats cluster %v: %w", options.ClusterID, err)
}
s.conn = conn
s.natsJetStreamCtx = natsJetStreamCtx
return s, nil
}
type stream struct {
opts Options
conn *nats.Conn // store connection for lifecycle management
natsJetStreamCtx nats.JetStreamContext
}
func connectToNatsJetStream(options Options) (*nats.Conn, nats.JetStreamContext, error) {
nopts := nats.GetDefaultOptions()
if options.TLSConfig != nil {
nopts.Secure = true
nopts.TLSConfig = options.TLSConfig
}
if options.NkeyConfig != "" {
nopts.Nkey = options.NkeyConfig
}
if len(options.Address) > 0 {
nopts.Servers = strings.Split(options.Address, ",")
}
if options.Name != "" {
nopts.Name = options.Name
}
if options.Username != "" && options.Password != "" {
nopts.User = options.Username
nopts.Password = options.Password
}
conn, err := nopts.Connect()
if err != nil {
tls := nopts.TLSConfig != nil
return nil, nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
}
js, err := conn.JetStream()
if err != nil {
conn.Close() // Close connection if JetStream context fails
return nil, nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
}
return conn, js, nil
}
// Publish a message to a topic.
func (s *stream) Publish(topic string, msg interface{}, opts ...events.PublishOption) error {
// validate the topic
if len(topic) == 0 {
return events.ErrMissingTopic
}
// parse the options
options := events.PublishOptions{
Timestamp: time.Now(),
}
for _, o := range opts {
o(&options)
}
// encode the message if it's not already encoded
var payload []byte
if p, ok := msg.([]byte); ok {
payload = p
} else {
p, err := json.Marshal(msg)
if err != nil {
return events.ErrEncodingMessage
}
payload = p
}
// construct the event
event := &events.Event{
ID: uuid.New().String(),
Topic: topic,
Timestamp: options.Timestamp,
Metadata: options.Metadata,
Payload: payload,
}
// serialize the event to bytes
bytes, err := json.Marshal(event)
if err != nil {
return errors.Wrap(err, "Error encoding event")
}
// publish the event to the topic's channel
// publish synchronously if configured
if s.opts.SyncPublish {
_, err := s.natsJetStreamCtx.Publish(event.Topic, bytes)
if err != nil {
err = errors.Wrap(err, "Error publishing message to topic")
}
return err
}
// publish asynchronously by default
if _, err := s.natsJetStreamCtx.PublishAsync(event.Topic, bytes); err != nil {
return errors.Wrap(err, "Error publishing message to topic")
}
return nil
}
// Consume from a topic.
func (s *stream) Consume(topic string, opts ...events.ConsumeOption) (<-chan events.Event, error) {
// validate the topic
if len(topic) == 0 {
return nil, events.ErrMissingTopic
}
log := s.opts.Logger
// parse the options
options := events.ConsumeOptions{
Group: uuid.New().String(),
}
for _, o := range opts {
o(&options)
}
// setup the subscriber
channel := make(chan events.Event)
handleMsg := func(msg *nats.Msg) {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
// decode the message
var evt events.Event
if err := json.Unmarshal(msg.Data, &evt); err != nil {
log.Logf(logger.ErrorLevel, "Error decoding message: %v", err)
// not acknowledging the message is the way to indicate an error occurred
return
}
if options.AutoAck {
// set up the ack funcs
evt.SetAckFunc(func() error {
return msg.Ack()
})
evt.SetNackFunc(func() error {
return msg.Nak()
})
} else {
// set up the ack funcs
evt.SetAckFunc(func() error {
return nil
})
evt.SetNackFunc(func() error {
return nil
})
}
// push onto the channel and wait for the consumer to take the event off before we acknowledge it.
channel <- evt
if !options.AutoAck {
return
}
if err := msg.Ack(nats.Context(ctx)); err != nil {
log.Logf(logger.ErrorLevel, "Error acknowledging message: %v", err)
}
}
// ensure that a stream exists for that topic
_, err := s.natsJetStreamCtx.StreamInfo(topic)
if err != nil {
cfg := &nats.StreamConfig{
Name: topic,
}
if s.opts.RetentionPolicy != 0 {
cfg.Retention = nats.RetentionPolicy(s.opts.RetentionPolicy)
}
if s.opts.MaxAge > 0 {
cfg.MaxAge = s.opts.MaxAge
}
_, err = s.natsJetStreamCtx.AddStream(cfg)
if err != nil {
return nil, errors.Wrap(err, "Stream did not exist and adding a stream failed")
}
}
// setup the options
subOpts := []nats.SubOpt{}
if options.CustomRetries {
subOpts = append(subOpts, nats.MaxDeliver(options.GetRetryLimit()))
}
if options.AutoAck {
subOpts = append(subOpts, nats.AckAll())
} else {
subOpts = append(subOpts, nats.AckExplicit())
}
if !options.Offset.IsZero() {
subOpts = append(subOpts, nats.StartTime(options.Offset))
} else {
subOpts = append(subOpts, nats.DeliverNew())
}
if options.AckWait > 0 {
subOpts = append(subOpts, nats.AckWait(options.AckWait))
}
// connect the subscriber via a queue group only if durable streams are enabled
if !s.opts.DisableDurableStreams {
subOpts = append(subOpts, nats.Durable(options.Group))
_, err = s.natsJetStreamCtx.QueueSubscribe(topic, options.Group, handleMsg, subOpts...)
} else {
subOpts = append(subOpts, nats.ConsumerName(options.Group))
_, err = s.natsJetStreamCtx.Subscribe(topic, handleMsg, subOpts...)
}
if err != nil {
return nil, errors.Wrap(err, "Error subscribing to topic")
}
return channel, nil
}
// Close implements io.Closer and closes the underlying NATS connection.
// This method is optional but recommended to prevent connection leaks.
func (s *stream) Close() error {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
return nil
}
// Ensure stream implements io.Closer
var _ io.Closer = (*stream)(nil)