Files
member-console/internal/workflows
cgalo5758 bfc24e2c74 Serve before Temporal connects, and connect it in the background
Sixth section of #141, which closes the issue's defect: start no longer
waits on Temporal before it serves. Start derives the Stripe key's mode
and fingerprint, sets the SDK's key, opens the database, applies the
operator overrides and the boot data state, binds the listener, and only
then starts the Temporal connector and the identity provider's discovery
beside the server. A bind error returns before any dial or discovery
request. While Temporal is unreachable the console serves: sign-in, the
domain ask, the static files and every page that needs no workflow
answer as usual, and the Temporal-backed actions answer as the previous
section made them.

The connector runs the retry loop. Each attempt dials once (NewClient
no longer retries on its own; dialWithRetry and its budget are gone),
builds the worker and runs its precondition. A precondition that
refuses keeps the client and retries at the cap, so FedWiki's refusal
over open runs of a removed workflow type is read again on the same
connection; one that cannot read what it checks, a worker that fails to
build or start, closes the client and dials again. The attempt ends when
the worker starts: "connected to Temporal server" is logged with the
attempts and the time it took, then the holder is published and the
integrations' Startup hooks and the schedules run as before. "Temporal not ready yet, retrying" is a Warn until
temporal-connect-timeout (90 seconds by default), whose help now says
so, and an Error from then on, under the loop's decay. A Temporal OAuth
configuration that cannot be built still refuses at start.

start exits 1 when serving fails, at any point, and returns only after
the connector and discovery have returned. A failure before the
listener keeps the message it had (the overrides, the boot data state,
the integration connections); a serving failure logs "server failed to
start".

The Stripe webhook activity marks an event processing only from
received, processing or failed, so a run of an event already completed,
skipped or dead-lettered returns at once and dispatches nothing.

The handlers and drains that found no Temporal client logged that it
was not configured; with Temporal still connecting that was false, so
they log "Temporal is not connected" (the six plan-act goldens carry the
new drain line). Copies 13, 15 and 16 also carry the host and namespace.

Checked by hand on the test stack: with Temporal stopped, the console
served /login and the ask at once; the connector logged Warn, then
Error from the first attempt after 90 seconds; when Temporal came back
the same process logged "connected to Temporal server" after 14
attempts. With the issuer refusing, /login answered the styled 503 with
Retry-After: 5 and the ask answered.

Full suite on reset databases: 4513 pass, 15 skip, none failed. go test -race -p 1 over cmd,
auth, server, retryloop, workflows and the three integrations after
reset-test-db.sh: every package passes, no data race. make lint passes. Breaks, each caught
(section 1's UseKey break among them). Two depart from item 6.10's
first wording, which the item now carries: the synchronous-fetch break
sits between listen and the server's start (before listen the row would
fail waiting for the address, not at its one-second assertions), and the
"returns without waiting" break is caught by the fourth boot row, whose
fake dial holds for 200 ms of wall-clock time past the context's end,
the least that makes an early return observable.
- serveConsole without UseKey
- UseKey after the bound server's Serve
- Serve without go Discover
- Serve fetches the discovery document before serving
- serveConsole waits for connectTemporal before Serve
- connectTemporal started before Bind returns
- serveConsole returns without waiting for its goroutines
- serveConsole returns without waiting for its goroutines (race)
- skipSession without the ask's path
- connectTemporal publishes after afterConnect
- connectTemporal publishes before worker.Start
- connectTemporal returns after the escalation time
- the open-runs refusal closes the client
- ErrCannotCheck treated as a refusal
- worker.Start failure leaves the client open
- listRunsError without its Is method
- worker.Start failure logged from inside the attempt
- ProcessWebhookEvent without its status guard
- the guard without failed
- the guard without processing
- NewClient without the deadline
- connectTemporal under serveConsole's own context
- the attempt runs the hooks before copy 14
- Run logs copy 21 for a step failure
- the overrides failure is a plain error
- a Temporal authorization that cannot be built is not refused
2026-10-23 20:45:22 -04:00
..
2025-12-19 15:47:32 -06:00

Temporal Workflows

This directory contains the Temporal workflow definitions, activity implementations, and worker configuration for the member-console application.

Structure: Domain-Based

We use a Domain-Based (or Feature-Based) structure. Instead of grouping all workflows in one folder and all activities in another, we group them by business domain.

internal/workflows/
├── client.go           # Temporal client factory
├── worker.go           # Central worker setup & registration
├── queues/
│   └── queues.go       # Task Queue constants
├── common/             # Shared utilities & generic activities
│   └── options.go      # Standard ActivityOptions (retries, timeouts)
├── example/            # "Example" Domain
│   ├── workflow.go     # Workflow definition
│   └── activities.go   # Domain-specific activities
├── fedwiki/            # "FedWiki" Domain - Site provisioning
│   ├── README.md       # Domain documentation
│   ├── FARMMANAGER.md  # FarmManager API documentation
│   ├── workflow.go     # Create/Delete site workflows
│   ├── activities.go   # Site management activities
│   └── farmmanager_client.go  # HTTP client for FarmManager API
└── onboarding/         # (Future) "Onboarding" Domain
    ├── workflow.go
    └── activities.go

Adding a New Domain

  1. Create a directory: internal/workflows/<domain_name>/
  2. Define the Workflow: Create workflow.go in that directory.
  3. Define Activities: Create activities.go in that directory.
    • Define a struct (e.g., Activities) to hold dependencies (DB, Logger).
    • Implement a NewActivities constructor.
  4. Register: Go to internal/workflows/worker.go and add your new domain to the NewWorker function:
// internal/workflows/worker.go

// ... inside NewWorker ...
w.RegisterWorkflow(yourdomain.Workflow)
w.RegisterActivity(yourdomain.NewActivities(cfg.DB, cfg.Logger))

Usage

Starting the Worker

The worker is embedded in the main application but can be extracted later.

import "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"

// ... in your main setup ...
workerCfg := workflows.DefaultWorkerConfig(database, logger)
worker, err := workflows.NewWorker(temporalClient, workerCfg)
if err := worker.Start(); err != nil {
    // handle error
}
defer worker.Stop()

Starting a Workflow

import (
    "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"
    "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/queues"
    "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/example"
)

// ... inside an HTTP handler ...
workflowOpts := client.StartWorkflowOptions{
    ID:        "example-" + uuid.New().String(),
    TaskQueue: queues.Main,
}

we, err := temporalClient.ExecuteWorkflow(
    ctx,
    workflowOpts,
    example.Workflow, // Pass the function reference
    example.Input{Message: "hello"},
)