Files
member-console/internal/workflows/fedwiki/workflow.go

209 lines
7.0 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.
// Uses exponential backoff: 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 128s -> 256s
// Total retry time approximately 8 minutes before giving up.
func FedWikiActivityOptions() workflow.ActivityOptions {
return workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Minute, // Allow time for retries within activity
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 5 * time.Minute,
MaximumAttempts: 8, // 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = ~255 seconds (~4 min), but with 8 attempts we get up to 256s
NonRetryableErrorTypes: []string{
"QuotaExceeded",
"InvalidDomain",
"Unauthorized",
"DomainAlreadyExists",
"FarmManagerAPIError", // Non-retryable API errors (409, 403, 401)
},
},
}
}
// 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 {
UserID int64
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 int64
Domain string
ErrorMessage string
}
// CreateFedWikiSiteWorkflow orchestrates the creation of a new FedWiki site.
func CreateFedWikiSiteWorkflow(ctx workflow.Context, input CreateFedWikiSiteWorkflowInput) (*CreateFedWikiSiteWorkflowOutput, error) {
logger := workflow.GetLogger(ctx)
logger.Info("CreateFedWikiSiteWorkflow started",
"userID", input.UserID,
"domain", input.Domain)
var activities *Activities
// Step 1: Check quota using local activity options (fast, local DB call)
localCtx := workflow.WithActivityOptions(ctx, LocalActivityOptions())
var quotaResult *CheckQuotaOutput
err := workflow.ExecuteActivity(localCtx, activities.CheckQuotaActivity, CheckQuotaInput{
UserID: input.UserID,
}).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("User quota exceeded",
"userID", input.UserID,
"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: Create the site using FedWiki activity options (external API with retries)
fedwikiCtx := workflow.WithActivityOptions(ctx, FedWikiActivityOptions())
var createResult *CreateFedWikiSiteOutput
err = workflow.ExecuteActivity(fedwikiCtx, activities.CreateFedWikiSiteActivity, CreateFedWikiSiteInput{
UserID: input.UserID,
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", "error", err)
// Try to extract user-friendly message from the error
// Activity errors wrap ApplicationErrors, so we need to use errors.As
var appErr *temporal.ApplicationError
var errorMsg string
if errors.As(err, &appErr) {
errorMsg = appErr.Message()
} else {
// Generic error message for unexpected failures
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 {
UserID int64
Domain string // Full domain as stored in database
SupportURL string
}
// DeleteFedWikiSiteWorkflowOutput is the output for the DeleteFedWikiSiteWorkflow.
type DeleteFedWikiSiteWorkflowOutput struct {
Success bool
ErrorMessage string
}
// DeleteFedWikiSiteWorkflow orchestrates the deletion of a FedWiki site.
func DeleteFedWikiSiteWorkflow(ctx workflow.Context, input DeleteFedWikiSiteWorkflowInput) (*DeleteFedWikiSiteWorkflowOutput, error) {
logger := workflow.GetLogger(ctx)
logger.Info("DeleteFedWikiSiteWorkflow started",
"userID", input.UserID,
"domain", input.Domain)
var activities *Activities
// Delete the site using FedWiki activity options (external API with retries)
fedwikiCtx := workflow.WithActivityOptions(ctx, FedWikiActivityOptions())
var deleteResult *DeleteFedWikiSiteOutput
err := workflow.ExecuteActivity(fedwikiCtx, activities.DeleteFedWikiSiteActivity, DeleteFedWikiSiteInput{
UserID: input.UserID,
Domain: input.Domain,
}).Get(ctx, &deleteResult)
if err != nil {
logger.Error("Failed to delete site after retries", "error", err)
// Try to extract user-friendly message from the error
// Activity errors wrap ApplicationErrors, so we need to use errors.As
var appErr *temporal.ApplicationError
var errorMsg string
if errors.As(err, &appErr) {
errorMsg = appErr.Message()
} else {
// Generic error message for unexpected failures
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
}
logger.Info("DeleteFedWikiSiteWorkflow completed successfully",
"domain", input.Domain)
return &DeleteFedWikiSiteWorkflowOutput{
Success: true,
}, nil
}