Queue (Go Cloud Pub/Sub + HTTP push)¶
Frame wraps Go Cloud Pub/Sub for pull transports and adds a first-class HTTP push path for Knative Eventing and Cloud Tasks. Application registration stays the same; mode is selected by URL scheme.
Concepts¶
Publisher: publish messages to a named queue.Subscriber: receive messages and dispatch to handlers.Manager: register and manage publishers/subscribers.- Push mux:
POST /_frame/queue/{reference}demultiplexes to push subscribers.
Quick Start (local)¶
_, svc := frame.NewService(
frame.WithRegisterPublisher("orders", "mem://orders"),
frame.WithRegisterSubscriber("orders", "mem://orders", handler),
)
_ = svc.QueueManager().Publish(ctx, "orders", OrderCreated{ID: "123"})
URL schemes¶
Subscribe¶
| Scheme | Mode | Behavior |
|---|---|---|
mem:// |
pull | In-memory gocloud |
nats:// |
pull | NATS / JetStream via natspubsub |
gcppubsub:// |
pull | GCP Pub/Sub StreamingPull via gocloud (gcppubsub driver; ADC or emulator). Frame blank-imports the driver — apps that import frame/v2 do not need their own blank import. |
push://{ref} |
push | HTTP only; demux by registration reference |
http(s)://… |
push | Completes latent stub; demux still by registration ref |
Query on push URLs: protocol=auto|raw|cloudevents|cloudtasks (default auto).
GCP Pub/Sub push vs pull: gcppubsub://…/subscriptions/… is always pull inside Frame (the process opens a subscription and receives). If you configure a GCP-side push subscription that POSTs to this service, register the subscriber as push://{ref} and point the push endpoint at https://…/_frame/queue/{ref} — do not use gcppubsub:// for that path (no gocloud receive loop; HTTP demux only).
Publish¶
| Scheme | Behavior |
|---|---|
mem:// / nats:// / gcppubsub:// |
gocloud OpenTopic |
ce+https://host/path?type=…&source=… |
CloudEvents 1.0 binary HTTP POST (ce+ stripped on the wire) |
cloudtasks:///projects/{p}/locations/{l}/queues/{q}?url=… |
Cloud Tasks CreateTask REST |
GCP topic URL forms (gocloud):
gcppubsub://projects/{project}/topics/{topic}
gcppubsub://{project}/{topic}
GCP subscription URL forms (pull):
gcppubsub://projects/{project}/subscriptions/{sub}
gcppubsub://{project}/{sub}
Canonical Cloud Tasks form (empty host):
cloudtasks:///projects/p/locations/l/queues/q?url=https%3A%2F%2Fsvc%2F_frame%2Fqueue%2Forders&oidc_sa=sa@proj.iam.gserviceaccount.com
Knative (same handlers)¶
frame.WithRegisterPublisher("orders",
"ce+https://broker-ingress.knative-eventing.svc.cluster.local/default/default"+
"?type=com.example.order.created&source=//orders-svc"),
frame.WithRegisterSubscriber("orders", "push://orders", handler{}),
// Cluster-internal: RUN_SERVICE_SECURELY=false or FRAME_QUEUE_PUSH_REQUIRE_AUTH=false
// or use bearer/OIDC as appropriate
Trigger:
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: orders-to-svc
spec:
broker: default
filter:
attributes:
type: com.example.order.created
subscriber:
uri: http://orders.default.svc.cluster.local/_frame/queue/orders
GCP Pub/Sub (Cloud Run)¶
Reliable path: publish to a topic via gocloud, receive via HTTP push into the Frame demux.
| Side | Env | Example |
|---|---|---|
| Name / demux ref | EVENTS_QUEUE_NAME |
identity-profile-events |
| Publish (topic) | EVENTS_QUEUE_PUBLISH_URL |
gcppubsub://my-project/identity-profile-events |
| Subscribe (HTTP) | EVENTS_QUEUE_SUBSCRIBE_URL |
push://identity-profile-events |
| Fallback both | EVENTS_QUEUE_URL |
used when publish/subscribe overrides are empty |
- Create a Pub/Sub topic (and optional regional
message_storage_policy). - Create a push subscription with endpoint
https://{service}/_frame/queue/{EVENTS_QUEUE_NAME}
and OIDC (FRAME_QUEUE_PUSH_AUTH=oidc). - Set
EVENTS_QUEUE_PUBLISH_URLto the topic URL andEVENTS_QUEUE_SUBSCRIBE_URLtopush://{name}. - Do not use
gcppubsub://…/subscriptions/…when the GCP subscription is push — that would open a StreamingPull loop instead of the HTTP handler.
// setupEventsQueue (automatic with WithRegisterEvents) uses:
// publisher = GetEventsQueuePublishURL() // gcppubsub://…
// subscriber = GetEventsQueueSubscribeURL() // push://…
// handlers = events.Manager.Handler()
Cloud Tasks¶
- Publisher URL with ADC credentials on the service (CreateTask API OAuth2).
- Subscriber
push://orderswithFRAME_QUEUE_PUSH_AUTH=oidc(optionally pin the invoker SA viaFRAME_QUEUE_PUSH_OIDC_ALLOWED_EMAILS). - Task
url=https://…/_frame/queue/orders. - Configure queue maxAttempts + DLQ (non-2xx always retry until maxAttempts).
Push HTTP status map¶
| Condition | Status |
|---|---|
| Success | 200 |
| Unknown / non-push ref | 404 |
| Bad method | 405 |
| Auth failure | 401 / 403 |
| Body too large | 413 |
| Decode error | 400 |
queue.ErrNotRetryable |
422 |
| Handler / overload / panic | 503 |
| Handler timeout | 504 |
Permanent poison: return fmt.Errorf("%w: …", queue.ErrNotRetryable). Optional FRAME_QUEUE_PUSH_ACK_POISON=true maps that to 200 (dangerous; off by default).
Security¶
| Env | Default | Meaning |
|---|---|---|
FRAME_QUEUE_PUSH_AUTH |
none |
none | bearer | oidc |
FRAME_QUEUE_PUSH_REQUIRE_AUTH |
inherits RUN_SERVICE_SECURELY |
fail start if push + auth=none |
FRAME_QUEUE_PUSH_BEARER_TOKEN |
bearer secret | |
FRAME_QUEUE_PUSH_OIDC_AUDIENCE |
request URL | JWT audience |
FRAME_QUEUE_PUSH_OIDC_ISSUERS |
Google accounts | comma-separated issuers |
FRAME_QUEUE_PUSH_OIDC_JWKS_URL |
Google certs | JWKS URL |
FRAME_QUEUE_PUSH_OIDC_ALLOWED_EMAILS |
empty | comma-separated SA emails / sub allowlist (empty = no principal filter) |
FRAME_QUEUE_PUSH_TRUST_METADATA |
false |
allow claim-shaped metadata from push |
FRAME_QUEUE_PUSH_MAX_BODY_BYTES |
1048576 |
body limit |
FRAME_QUEUE_PUSH_HANDLER_TIMEOUT |
0s (no timeout) |
optional hard push SLA |
FRAME_QUEUE_PUSH_BASE_PATH |
/_frame/queue |
reserved mux path |
By default, push strips claim keys (sub, tenant_id, partition_id,
partition_ids, roles, …) so handlers cannot be fooled by forged headers.
Pull consumers default to TrustAll (legacy). Secure Profile uses
frame.WithQueueClaimTrust(protocol.TrustTenancyOnly) so roles /
service_name cannot force RLS Skip from metadata while still binding
tenant/partition from trusted publishers. Residual risk: forged
tenant_id on an open broker — use private mesh, per-tenant topics, or
TrustNone + handler-side tenancy.WithClaims.
OIDC for Cloud Tasks / GCP push uses a dedicated Google JWKS validator — not the app Hydra JWT authenticator. When FRAME_QUEUE_PUSH_OIDC_ALLOWED_EMAILS is set, both JWT email and sub are checked against the allowlist (case-insensitive); a match on either claim is enough. Mismatch on both → 403.
Example (Cloud Tasks or Pub/Sub push from a known SA):
FRAME_QUEUE_PUSH_AUTH=oidc
FRAME_QUEUE_PUSH_OIDC_AUDIENCE=https://orders.example.com/_frame/queue/orders
FRAME_QUEUE_PUSH_OIDC_ALLOWED_EMAILS=tasks-invoker@my-proj.iam.gserviceaccount.com,pubsub-push@my-proj.iam.gserviceaccount.com
Subscriber handlers¶
type handler struct{}
func (h handler) Handle(ctx context.Context, metadata map[string]string, message []byte) error {
return nil
}
Pull and push share the same processDelivery path (context enrichment policy, worker pool, metrics).
CloudEvents ↔ Frame events¶
- Egress maps
frame._internal.event.header→ CE extensionframeevent. - Ingress maps
ce-frameevent→frame._internal.event.header. - There is no silent alias of
ce-type→ event name.
Manager API¶
AddPublisher/AddSubscriber/Publish/Init/CloseHasPushSubscribers/LookupPushSubscriber.Mode()→ pull or push
Metrics¶
Subscribers expose SubscriberMetrics (idle / processing). Push increments active messages inside processDeliverySync.
Best Practices¶
- Register publishers before subscribers (Frame enforces ordering).
- Keep handler work short; offload heavy work to the worker pool carefully (do not submit-and-wait pool jobs from inside a push handler job).
- Use structured metadata for trace correlation (
traceparent). - Prefer
push://{ref}over rawhttps://…subscriber URLs for clarity. - Local push testing:
FRAME_QUEUE_PUSH_REQUIRE_AUTH=falseor bearer with a dev token.