mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
Every import in this module now resolves to github.com/hanzoai/gateway/v2.
The 34 github.com/krakend/* + github.com/luraproject/lura modules that
supplied the engine and its components are vendored under internal/ and
their 91 packages rewritten onto our own path.
Why fold rather than mirror into 34 hanzoai/* repos:
- It removes the exposure outright. 34 rows leave 34 rows of go.sum to
reason about; zero modules leave nothing. `go list -m all` and go.sum
now contain no krakend/lura entry at all.
- Cross-references cannot go wrong. 28 of the 34 import lura (97 files
inside lura self-reference it). Mirroring means rewriting every one of
those to our lura, and a single miss silently links a SECOND copy of
lura into the binary. With one module that is structurally impossible.
- It creates no duplicates. These are lura-coupled gateway glue with no
consumer outside this binary; publishing 34 repos would be 34 new
things to maintain for zero second caller. internal/ makes that
guarantee mechanical — Go forbids importing it from outside this
module, so none of it can become a competing public package. Verified
no repo outside gateway imports any hanzoai/gateway/v2 subpackage.
Layout mirrors provenance so an upstream re-sync stays a diff:
luraproject/lura/v2 -> internal/lura (the engine, 20 pkgs)
krakend/krakend-<x> -> internal/plugin/<x> (28 components)
krakend/<x> -> internal/pkg/<x> (6 standalone libs)
This commit is PURELY MECHANICAL: 196 of the 197 vendored files are
byte-identical to their upstream original once the import rewrite is
applied. The single exception is bloomfilter's subpackage, which upstream
names `krakend` — the one package name that could not survive as-is; it
becomes internal/pkg/bloomfilter/register. Hand edits land separately in
the next commit so they are readable on their own.
Per `go mod vendor` semantics, dependencies' _test.go files are not
vendored. They test upstream code at a pinned version that we do not
modify, several cannot run at all from a module distribution (they need
TLS certs and compiled .so plugins upstream generates but never ships),
and one is a 100k-request stress test that exhausts the ephemeral port
range and fails every package running beside it. Fold integrity is proven
by the byte-diff above instead, which is the stronger check.
Preserved deliberately — each of these is behaviour, not branding:
- the `github.com/devopsfaith/krakend*` extra_config namespace strings
(35 of them). These are config-surface keys, not import paths;
renaming one silently stops that component finding its config. The
jose validator namespace backs `auth/validator` on 212 endpoints.
- core.KrakendVersion / KrakendUserAgent / the X-KRAKEND consts, whose
VALUES rebrand.go overwrites — that seam is unchanged.
- the KRAKEND_ koanf env prefix (a hardcoded upstream const).
- every vendored module's LICENSE (34 files, Apache-2.0/MIT).
The Makefile/CI -X paths move with the engine. An -X naming a symbol that
does not exist is silently ignored by the linker, so a stale path here
would have shipped an "undefined" version in the backend User-Agent and
X-Api-Version with a perfectly green build. Verified live:
`gateway version` reports 2.13.0, not undefined.
Verification (GOWORK=off CGO_ENABLED=0, both tag modes):
go build ./... OK go build -tags legacy ./... OK
go vet ./... clean go vet -tags legacy ./... clean
go test ./... 6/6 ok go test -tags legacy 5/5 ok
tests/ integration harness 47 fixtures pass
gateway check -c configs/{hanzo,lux}/gateway.json -> Syntax OK!
go list -m all | grep -iE 'krakend|lura' -> empty
go mod verify -> all modules verified
Co-authored-by: Hanzo Dev <[email protected]>
308 lines
7.2 KiB
Go
308 lines
7.2 KiB
Go
package async
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
|
|
ratelimit "github.com/hanzoai/gateway/v2/internal/plugin/ratelimit"
|
|
|
|
"github.com/hanzoai/gateway/v2/internal/lura/config"
|
|
"github.com/hanzoai/gateway/v2/internal/lura/logging"
|
|
"github.com/hanzoai/gateway/v2/internal/lura/proxy"
|
|
)
|
|
|
|
// Subscriber defines the configuration of a single subscriber/consumer to be initialized
|
|
// and maintained by the lura service
|
|
type Subscriber struct {
|
|
Name string
|
|
Topic string
|
|
// pipe definition
|
|
Endpoint *config.EndpointConfig
|
|
// timeout of the pipe defined by this subscriber
|
|
Timeout time.Duration
|
|
// Endpoint Extra configuration for customized behaviour
|
|
ExtraConfig config.ExtraConfig
|
|
Workers int
|
|
MaxRate float64
|
|
}
|
|
|
|
const consumerNamespace = "github.com/devopsfaith/krakend-amqp/agent"
|
|
|
|
type Options struct {
|
|
Logger logging.Logger
|
|
Proxy proxy.Proxy
|
|
Ping chan<- string
|
|
PingTicker *time.Ticker
|
|
}
|
|
|
|
// New instantiates and executes an async agent consuming from a rabbitmq
|
|
// service. The caller is responsible for reconnections.
|
|
func New(ctx context.Context, cfg Subscriber, opts Options) error { // skipcq: GO-R1005
|
|
v, ok := cfg.ExtraConfig[consumerNamespace]
|
|
if !ok {
|
|
return ErrNoConsumerCfgDefined
|
|
}
|
|
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
consumrCfg := consumerCfg{}
|
|
if err := json.Unmarshal(b, &consumrCfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
options := consumerOptions{
|
|
consumerCfg: consumrCfg,
|
|
Topic: cfg.Topic,
|
|
Timeout: cfg.Timeout,
|
|
}
|
|
|
|
opts.Logger.Info(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Starting the consumer", cfg.Name))
|
|
msgs, closeF, err := initConsumer(options, opts.Logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeF()
|
|
|
|
// initial ping
|
|
if cap(opts.Ping) < 1 {
|
|
opts.Logger.Warning(
|
|
fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Ping channel with 0 capacity might block this async agent",
|
|
cfg.Name))
|
|
}
|
|
opts.Ping <- cfg.Name
|
|
|
|
shouldAck := newProcessor(ctx, cfg, opts.Logger, opts.Proxy)
|
|
if cfg.Workers < 1 {
|
|
// If the number of workers is 0, this
|
|
// we probably need to check that the minimum amount of workers is 1
|
|
opts.Logger.Error(
|
|
fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] With less than 1 worker this agent does no work", cfg.Name))
|
|
}
|
|
sem := make(chan struct{}, cfg.Workers)
|
|
var shouldExit atomic.Value
|
|
shouldExit.Store(false)
|
|
defer opts.PingTicker.Stop()
|
|
|
|
waitIfRequired := func() {}
|
|
if cfg.MaxRate > 0 {
|
|
capacity := uint64(cfg.MaxRate)
|
|
if capacity == 0 {
|
|
capacity = 1
|
|
}
|
|
bucket := ratelimit.NewTokenBucket(cfg.MaxRate, capacity)
|
|
// We wait enough time to allow to have at least an extra token
|
|
// (even some other goroutine might have consumed while we wait)
|
|
pollingTime := time.Nanosecond * time.Duration(1e9/cfg.MaxRate)
|
|
waitIfRequired = func() {
|
|
for !bucket.Allow() {
|
|
time.Sleep(pollingTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
recvLoop:
|
|
for !shouldExit.Load().(bool) {
|
|
// block until an event happens
|
|
// 1: semaphore acquired
|
|
// 2: context ended
|
|
// 3: ping ticker
|
|
select {
|
|
case sem <- struct{}{}:
|
|
// acquire the semaphore
|
|
case <-ctx.Done():
|
|
// agent should stop
|
|
err = ctx.Err()
|
|
break recvLoop
|
|
case <-opts.PingTicker.C:
|
|
// time to ping the router
|
|
opts.Ping <- cfg.Name
|
|
continue
|
|
}
|
|
|
|
go func() {
|
|
// release the semaphore
|
|
defer func() { <-sem }()
|
|
|
|
var msg amqp.Delivery
|
|
var more bool
|
|
|
|
select {
|
|
case <-ctx.Done(): // agent should stop
|
|
return
|
|
case msg, more = <-msgs: // block until a message is read or the chan is closed
|
|
if !more { // channel is closed
|
|
shouldExit.Store(true)
|
|
return
|
|
}
|
|
}
|
|
|
|
waitIfRequired()
|
|
|
|
ok := shouldAck(msg.Body)
|
|
if consumrCfg.AutoACK {
|
|
// the driver acks the message itself
|
|
return
|
|
}
|
|
|
|
if ok {
|
|
if err := msg.Ack(false); err != nil {
|
|
opts.Logger.Error(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Ack: %s", cfg.Name, err))
|
|
shouldExit.Store(true)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := msg.Nack(false, !consumrCfg.NackDiscard); err != nil {
|
|
opts.Logger.Error(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Nack: %s", cfg.Name, err))
|
|
shouldExit.Store(true)
|
|
} else if consumrCfg.NackDiscard {
|
|
opts.Logger.Debug(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Nack: message discarded from queue %q", cfg.Name, options.Name))
|
|
}
|
|
}()
|
|
}
|
|
|
|
opts.Logger.Warning(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] Consumer stopped", cfg.Name))
|
|
|
|
// the error is not nil, only when the context is Done() and has an error:
|
|
return err
|
|
}
|
|
|
|
type consumerCfg struct {
|
|
Host string `json:"host"`
|
|
Name string `json:"name"`
|
|
Exchange string `json:"exchange"`
|
|
Durable bool `json:"durable"`
|
|
Delete bool `json:"delete"`
|
|
Exclusive bool `json:"exclusive"`
|
|
NoWait bool `json:"no_wait"`
|
|
PrefetchCount int `json:"prefetch_count"`
|
|
PrefetchSize int `json:"prefetch_size"`
|
|
AutoACK bool `json:"auto_ack"`
|
|
NoLocal bool `json:"no_local"`
|
|
NackDiscard bool `json:"nack_discard"`
|
|
}
|
|
|
|
type consumerOptions struct {
|
|
consumerCfg
|
|
Topic string
|
|
Encoding string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
func newProcessor(ctx context.Context, cfg Subscriber, logger logging.Logger, next proxy.Proxy) func([]byte) bool {
|
|
return func(msg []byte) bool {
|
|
req := proxy.Request{
|
|
Params: map[string]string{},
|
|
Headers: map[string][]string{},
|
|
Body: io.NopCloser(bytes.NewBuffer(msg)),
|
|
}
|
|
contxt, cancel := context.WithTimeout(ctx, cfg.Timeout)
|
|
defer cancel()
|
|
|
|
_, err := next(contxt, &req)
|
|
|
|
if err == nil {
|
|
return true
|
|
}
|
|
logger.Error(fmt.Sprintf("[SERVICE: AsyncAgent][AMQP][%s] %s", cfg.Name, err.Error()))
|
|
return false
|
|
}
|
|
}
|
|
|
|
func initConsumer(opts consumerOptions, _ logging.Logger) (msgs <-chan amqp.Delivery, closeF func(), err error) {
|
|
closeF = func() {}
|
|
|
|
conn, err1 := amqp.Dial(opts.Host)
|
|
if err1 != nil {
|
|
err = err1
|
|
return
|
|
}
|
|
|
|
ch, err1 := conn.Channel()
|
|
if err1 != nil {
|
|
conn.Close()
|
|
err = err1
|
|
return
|
|
}
|
|
|
|
closeF = func() {
|
|
ch.Close()
|
|
conn.Close()
|
|
}
|
|
|
|
err = ch.ExchangeDeclare(
|
|
opts.Exchange, // name
|
|
"topic", // type
|
|
opts.Durable, // durable
|
|
opts.Delete, // delete when unused
|
|
opts.Exclusive, // exclusive
|
|
opts.NoWait, // no-wait
|
|
nil, // arguments
|
|
)
|
|
if err != nil {
|
|
closeF()
|
|
return
|
|
}
|
|
|
|
q, err1 := ch.QueueDeclare(
|
|
opts.Name, // name
|
|
opts.Durable, // durable
|
|
opts.Delete, // delete when unused
|
|
opts.Exclusive, // exclusive
|
|
opts.NoWait, // no-wait
|
|
nil, // arguments
|
|
)
|
|
if err1 != nil {
|
|
closeF()
|
|
err = err1
|
|
return
|
|
}
|
|
err = ch.QueueBind(
|
|
q.Name, // queue name
|
|
opts.Topic, // routing key
|
|
opts.Exchange, // exchange
|
|
false,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
closeF()
|
|
return
|
|
}
|
|
|
|
if opts.PrefetchCount != 0 || opts.PrefetchSize != 0 {
|
|
if err = ch.Qos(opts.PrefetchCount, opts.PrefetchSize, false); err != nil {
|
|
closeF()
|
|
return
|
|
}
|
|
}
|
|
|
|
msgs, err = ch.Consume(
|
|
q.Name, // queue
|
|
"krakend", // consumer
|
|
opts.AutoACK, // auto-ack
|
|
opts.Exclusive, // exclusive
|
|
opts.NoLocal, // no-local
|
|
opts.NoWait, // no-wait
|
|
nil, // args
|
|
)
|
|
if err != nil {
|
|
closeF()
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
var ErrNoConsumerCfgDefined = errors.New("no amqp consumer defined")
|