Files
member-console/internal/workflows/fedwiki/workflow.go
2026-03-24 17:35:14 -05:00

242 lines
8.1 KiB
Go

package fedwiki
import (
"errors"
"fmt"
"time"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
// FedWikiActivityOptions returns activity options for FedWiki API calls.
func FedWikiActivityOptions() workflow.ActivityOptions {
return workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 5 * time.Minute,
MaximumAttempts: 8,
NonRetryableErrorTypes: []string{
"QuotaExceeded",
"InvalidDomain",
"Unauthorized",
"DomainAlreadyExists",
"FarmManagerAPIError",
},
},
}
}
// LocalActivityOptions returns activity options for local database operations.
func LocalActivityOptions() workflow.ActivityOptions {
return workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 30 * time.Second,
MaximumAttempts: 3,
},
}
}
// CreateFedWikiSiteWorkflowInput is the input for the CreateFedWikiSiteWorkflow.
type CreateFedWikiSiteWorkflowInput struct {
WorkspaceID string
Domain string // The subdomain part (e.g., "mysite")
SiteDomain string // The parent domain (e.g., "localtest.me") - ignored for custom domains
OwnerName string // Display name for the owner
OwnerID string // OIDC subject (OAuth2 ID) for owner verification
IsCustomDomain bool
SupportURL string
}
// CreateFedWikiSiteWorkflowOutput is the output for the CreateFedWikiSiteWorkflow.
type CreateFedWikiSiteWorkflowOutput struct {
Success bool
SiteID string
Domain string
ErrorMessage string
}
// CreateFedWikiSiteWorkflow orchestrates the creation of a new FedWiki site.
// It uses a saga pattern: increment usage first, then create the site on the
// external farm. If creation fails, usage is compensated (decremented).
func CreateFedWikiSiteWorkflow(ctx workflow.Context, input CreateFedWikiSiteWorkflowInput) (*CreateFedWikiSiteWorkflowOutput, error) {
logger := workflow.GetLogger(ctx)
logger.Info("CreateFedWikiSiteWorkflow started",
"workspaceID", input.WorkspaceID,
"domain", input.Domain)
var activities *Activities
// Step 1: Check quota (read-only, fast)
localCtx := workflow.WithActivityOptions(ctx, LocalActivityOptions())
var quotaResult *CheckQuotaOutput
err := workflow.ExecuteActivity(localCtx, activities.CheckQuotaActivity, CheckQuotaInput{
WorkspaceID: input.WorkspaceID,
}).Get(ctx, &quotaResult)
if err != nil {
logger.Error("Failed to check quota", "error", err)
return &CreateFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: "Failed to verify your site quota. Please try again later.",
}, nil
}
if !quotaResult.CanCreate {
logger.Info("Workspace quota exceeded",
"workspaceID", input.WorkspaceID,
"currentCount", quotaResult.CurrentCount,
"quota", quotaResult.Quota)
return &CreateFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: fmt.Sprintf("You have reached your site limit (%d of %d sites). Join us and become a member for more sites.", quotaResult.CurrentCount, quotaResult.Quota),
}, nil
}
// Step 2: Atomically increment usage (reserve the slot)
err = workflow.ExecuteActivity(localCtx, activities.IncrementUsageActivity, IncrementUsageInput{
WorkspaceID: input.WorkspaceID,
}).Get(ctx, nil)
if err != nil {
logger.Error("Failed to increment usage", "error", err)
var appErr *temporal.ApplicationError
if errors.As(err, &appErr) && appErr.Type() == "QuotaExceeded" {
return &CreateFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: fmt.Sprintf("You have reached your site limit (%d of %d sites). Join us and become a member for more sites.", quotaResult.CurrentCount, quotaResult.Quota),
}, nil
}
return &CreateFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: "Failed to reserve site quota. Please try again later.",
}, nil
}
// Step 3: Create the site on the external farm (with retries)
fedwikiCtx := workflow.WithActivityOptions(ctx, FedWikiActivityOptions())
var createResult *CreateFedWikiSiteOutput
err = workflow.ExecuteActivity(fedwikiCtx, activities.CreateFedWikiSiteActivity, CreateFedWikiSiteInput{
WorkspaceID: input.WorkspaceID,
Domain: input.Domain,
SiteDomain: input.SiteDomain,
OwnerName: input.OwnerName,
OwnerID: input.OwnerID,
IsCustomDomain: input.IsCustomDomain,
}).Get(ctx, &createResult)
if err != nil {
logger.Error("Failed to create site after retries, compensating usage", "error", err)
// Compensate: decrement usage since site creation failed
compErr := workflow.ExecuteActivity(localCtx, activities.DecrementUsageActivity, DecrementUsageInput{
WorkspaceID: input.WorkspaceID,
}).Get(ctx, nil)
if compErr != nil {
logger.Error("Failed to compensate usage decrement", "error", compErr)
}
var appErr *temporal.ApplicationError
var errorMsg string
if errors.As(err, &appErr) {
errorMsg = appErr.Message()
} else {
errorMsg = "We encountered an issue creating your site. Our system tried multiple times but was unable to complete the request. "
errorMsg += "Please try again later. "
if input.SupportURL != "" {
errorMsg += fmt.Sprintf("If the problem persists, please contact support: %s", input.SupportURL)
}
}
return &CreateFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: errorMsg,
}, nil
}
logger.Info("CreateFedWikiSiteWorkflow completed successfully",
"siteID", createResult.SiteID,
"domain", createResult.Domain)
return &CreateFedWikiSiteWorkflowOutput{
Success: true,
SiteID: createResult.SiteID,
Domain: createResult.Domain,
}, nil
}
// DeleteFedWikiSiteWorkflowInput is the input for the DeleteFedWikiSiteWorkflow.
type DeleteFedWikiSiteWorkflowInput struct {
Domain string // Full domain as stored in database
WorkspaceID string // Workspace that owns the site, for usage decrement
SupportURL string
}
// DeleteFedWikiSiteWorkflowOutput is the output for the DeleteFedWikiSiteWorkflow.
type DeleteFedWikiSiteWorkflowOutput struct {
Success bool
ErrorMessage string
}
// DeleteFedWikiSiteWorkflow orchestrates the deletion of a FedWiki site
// and decrements entitlement usage on success.
func DeleteFedWikiSiteWorkflow(ctx workflow.Context, input DeleteFedWikiSiteWorkflowInput) (*DeleteFedWikiSiteWorkflowOutput, error) {
logger := workflow.GetLogger(ctx)
logger.Info("DeleteFedWikiSiteWorkflow started",
"domain", input.Domain,
"workspaceID", input.WorkspaceID)
var activities *Activities
// Step 1: Delete the site (external API + local DB)
fedwikiCtx := workflow.WithActivityOptions(ctx, FedWikiActivityOptions())
var deleteResult *DeleteFedWikiSiteOutput
err := workflow.ExecuteActivity(fedwikiCtx, activities.DeleteFedWikiSiteActivity, DeleteFedWikiSiteInput{
Domain: input.Domain,
}).Get(ctx, &deleteResult)
if err != nil {
logger.Error("Failed to delete site after retries", "error", err)
var appErr *temporal.ApplicationError
var errorMsg string
if errors.As(err, &appErr) {
errorMsg = appErr.Message()
} else {
errorMsg = "We encountered an issue deleting your site. Our system tried multiple times but was unable to complete the request. "
errorMsg += "Please try again later. "
if input.SupportURL != "" {
errorMsg += fmt.Sprintf("If the problem persists, please contact support: %s", input.SupportURL)
}
}
return &DeleteFedWikiSiteWorkflowOutput{
Success: false,
ErrorMessage: errorMsg,
}, nil
}
// Step 2: Decrement usage after successful deletion
if input.WorkspaceID != "" {
localCtx := workflow.WithActivityOptions(ctx, LocalActivityOptions())
err = workflow.ExecuteActivity(localCtx, activities.DecrementUsageActivity, DecrementUsageInput{
WorkspaceID: input.WorkspaceID,
}).Get(ctx, nil)
if err != nil {
logger.Error("Failed to decrement usage after site deletion", "error", err)
// Site is already deleted, so we log but don't fail the workflow
}
}
logger.Info("DeleteFedWikiSiteWorkflow completed successfully",
"domain", input.Domain)
return &DeleteFedWikiSiteWorkflowOutput{
Success: true,
}, nil
}