diff --git a/workflows/multiapp/E-CommerceArchitecture.png b/workflows/multiapp/E-CommerceArchitecture.png new file mode 100644 index 000000000..b798e908c Binary files /dev/null and b/workflows/multiapp/E-CommerceArchitecture.png differ diff --git a/workflows/multiapp/README.md b/workflows/multiapp/README.md new file mode 100644 index 000000000..edd822d1c --- /dev/null +++ b/workflows/multiapp/README.md @@ -0,0 +1,117 @@ +# Multi-App E-commerce Workflow Demo + +This demo showcases a realistic e-commerce order processing scenario using Dapr's multi-application workflow capabilities. The scenario demonstrates how a Go application can orchestrate complex business processes across multiple Java services, including AI-powered recommendations. + +## Architecture + +![E-commerce Architecture](E-CommerceArchitecture.png) + +The demo consists of four applications working together: + +1. **Go Order Orchestrator** - Main workflow coordinator +2. **Java Payment Service** - Payment processing +3. **Java Inventory Service** - Inventory management and reservation +4. **Java AI Recommendation Service** - AI-powered product recommendations + +## Prerequisites + +- Dapr CLI installed and initialized (`dapr init`) +- Go installed +- Java and Maven installed + +## Quick Start + +### 1. Build All Services + +```bash +make build +``` + +### 2. Run All Services + +#### Option A: Run with Multi-App Configuration (Recommended) + +```bash +dapr run -f dapr.yaml +``` + +#### Option B: Run in Separate Terminals + +**Terminal 1 - Go Order Orchestrator:** +```bash +make run-go +``` + +**Terminal 2 - Java Payment Service:** +```bash +make run-payment +``` + +**Terminal 3 - Java Inventory Service:** +```bash +make run-inventory +``` + +**Terminal 4 - Java AI Recommendation Service:** +```bash +make run-ai +``` + +### 3. Test the Workflow + +Send a POST request to start the workflow: + +```bash +curl -X POST http://localhost:50001/start-workflow +``` + +## Service Ports + +- **Go Order Orchestrator**: 50001 (Dapr HTTP: 3505) +- **Java Payment Service**: 50002 (Dapr HTTP: 3506) +- **Java Inventory Service**: 50003 (Dapr HTTP: 3507) +- **Java AI Recommendation Service**: 50004 (Dapr HTTP: 3508) + +## Development + +### Building Individual Services + +```bash +# Go Order Orchestrator +cd go/order-orchestrator +go mod tidy +go run . + +# Java Payment Service +cd java/payment-service +mvn clean package +java -jar target/payment-service-1.0.0.jar + +# Java Inventory Service +cd java/inventory-service +mvn clean package +java -jar target/inventory-service-1.0.0.jar + +# Java AI Recommendation Service +cd java/ai-recommendation-service +mvn clean package +java -jar target/ai-recommendation-service-1.0.0.jar +``` + +### Testing + +```bash +make test +``` + +### Cleanup + +```bash +make clean +``` + +## Related Resources + +- [Multi-Application Workflows Documentation](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-multi-app/) +- [Multi-Application Java Workflows Example](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/workflows/multiapp) +- [Multi-Application Spring Boot Workflows Example](https://github.com/dapr/java-sdk/tree/master/spring-boot-examples/workflows/multi-app) diff --git a/workflows/multiapp/components/statestore.yaml b/workflows/multiapp/components/statestore.yaml new file mode 100644 index 000000000..02561e72f --- /dev/null +++ b/workflows/multiapp/components/statestore.yaml @@ -0,0 +1,16 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + - name: enableTLS + value: false + - name: actorStateStore + value: "true" diff --git a/workflows/multiapp/dapr.yaml b/workflows/multiapp/dapr.yaml new file mode 100644 index 000000000..b33d24198 --- /dev/null +++ b/workflows/multiapp/dapr.yaml @@ -0,0 +1,28 @@ +version: 1 +common: + resourcesPath: ./components +apps: + - appID: order-orchestrator + appDirPath: ./go/order-orchestrator/ + appPort: 50001 + daprHTTPPort: 3505 + daprGRPCPort: 50014 + command: ["go", "run", "."] + - appID: payment-service + appDirPath: ./java/payment-service/ + appPort: 50002 + daprHTTPPort: 3506 + daprGRPCPort: 50015 + command: ["java", "-jar", "target/payment-service-1.0.0.jar"] + - appID: inventory-service + appDirPath: ./java/inventory-service/ + appPort: 50003 + daprHTTPPort: 3507 + daprGRPCPort: 50016 + command: ["java", "-jar", "target/inventory-service-1.0.0.jar"] + - appID: ai-recommendation-service + appDirPath: ./java/ai-recommendation-service/ + appPort: 50004 + daprHTTPPort: 3508 + daprGRPCPort: 50017 + command: ["java", "-jar", "target/ai-recommendation-service-1.0.0.jar"] diff --git a/workflows/multiapp/go/order-orchestrator/go.mod b/workflows/multiapp/go/order-orchestrator/go.mod new file mode 100644 index 000000000..bab765c75 --- /dev/null +++ b/workflows/multiapp/go/order-orchestrator/go.mod @@ -0,0 +1,30 @@ +module order-orchestrator + +go 1.24.6 + +toolchain go1.24.7 + +require ( + github.com/dapr/durabletask-go v0.10.0 + github.com/dapr/go-sdk v1.13.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/dapr/dapr v1.16.0 // indirect + github.com/dapr/kit v0.16.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/workflows/multiapp/go/order-orchestrator/go.sum b/workflows/multiapp/go/order-orchestrator/go.sum new file mode 100644 index 000000000..6fd6362ab --- /dev/null +++ b/workflows/multiapp/go/order-orchestrator/go.sum @@ -0,0 +1,62 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/dapr/dapr v1.16.0 h1:la2WLZM8Myr2Pq3cyrFjHKWDSPYLzGZCs3p502TwBjI= +github.com/dapr/dapr v1.16.0/go.mod h1:ln/mxvNOeqklaDmic4ppsxmnjl2D/oZGKaJy24IwaEY= +github.com/dapr/durabletask-go v0.10.0 h1:vfIivPl4JYd55xZTslDwhA6p6F8ipcNxBtMaupxArr8= +github.com/dapr/durabletask-go v0.10.0/go.mod h1:0Ts4rXp74JyG19gDWPcwNo5V6NBZzhARzHF5XynmA7Q= +github.com/dapr/go-sdk v1.13.0 h1:Qw2BmUonClQ9yK/rrEEaFL1PyDgq616RrvYj0CT67Lk= +github.com/dapr/go-sdk v1.13.0/go.mod h1:RsffVNZitDApmQqoS68tNKGMXDZUjTviAbKZupJSzts= +github.com/dapr/kit v0.16.1 h1:MqLAhHVg8trPy2WJChMZFU7ToeondvxcNHYVvMDiVf4= +github.com/dapr/kit v0.16.1/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/workflows/multiapp/go/order-orchestrator/main.go b/workflows/multiapp/go/order-orchestrator/main.go new file mode 100644 index 000000000..a3604dea3 --- /dev/null +++ b/workflows/multiapp/go/order-orchestrator/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "github.com/dapr/durabletask-go/workflow" + "github.com/dapr/go-sdk/client" +) + +func main() { + // Create a workflow registry + registry := workflow.NewRegistry() + + // Register the workflow + registry.AddWorkflow(OrderProcessingWorkflow) + + log.Println("OrderProcessingWorkflow registered") + + // Register local activities + registry.AddActivity(ValidateOrderActivity) + registry.AddActivity(CompleteOrderActivity) + + log.Println("ValidateOrderActivity registered") + log.Println("CompleteOrderActivity registered") + + // Create a workflow client using the new vanity client + wclient, err := client.NewWorkflowClient() + if err != nil { + log.Fatalf("Failed to create workflow client: %v", err) + } + log.Println("Workflow client initialized") + + // Start the workflow worker + ctx := context.Background() + if err := wclient.StartWorker(ctx, registry); err != nil { + log.Fatalf("Failed to start workflow worker: %v", err) + } + log.Println("Workflow worker started") + + // Start HTTP server for health checks and workflow triggering + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Order Orchestrator is running")) + }) + + http.HandleFunc("/start-workflow", func(w http.ResponseWriter, r *http.Request) { + log.Println("Starting workflow execution via HTTP endpoint...") + + // Create a sample order + order := OrderRequest{ + OrderID: "ORDER-001", + CustomerID: "CUST-001", + Items: []Item{ + {ProductID: "PROD-001", Name: "Laptop", Price: 999.99, Quantity: 1}, + {ProductID: "PROD-002", Name: "Mouse", Price: 29.99, Quantity: 2}, + }, + PaymentMethod: "credit_card", + } + // Calculate the total from items + order.CalculateTotal() + + log.Printf("Scheduling workflow with input: %+v", order) + instanceID := fmt.Sprintf("ORDER-%d", time.Now().Unix()) + _, err := wclient.ScheduleWorkflow(ctx, "OrderProcessingWorkflow", + workflow.WithInstanceID(instanceID), + workflow.WithInput(order)) + if err != nil { + log.Printf("Failed to start workflow: %v", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Failed to start workflow: %v", err))) + return + } + + log.Printf("Workflow scheduled successfully with instance ID: %s", instanceID) + + log.Println("Waiting for workflow completion...") + waitCtx, waitCancel := context.WithTimeout(context.Background(), 60*time.Second) + result, err := wclient.WaitForWorkflowCompletion(waitCtx, instanceID) + waitCancel() + if err != nil { + log.Printf("Failed to wait for workflow completion: %v", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Failed to wait for workflow completion: %v", err))) + } else { + log.Printf("Workflow completed successfully with result: %+v", result) + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("Workflow completed successfully: %+v", result))) + } + }) + + log.Println("Starting HTTP server on port 50001...") + log.Fatal(http.ListenAndServe(":50001", nil)) +} + +// OrderProcessingWorkflow orchestrates the entire order processing workflow +func OrderProcessingWorkflow(ctx *workflow.WorkflowContext) (any, error) { + log.Println("=== OrderProcessingWorkflow STARTED ===") + + var input OrderRequest + if err := ctx.GetInput(&input); err != nil { + return nil, fmt.Errorf("failed to get workflow input: %w", err) + } + log.Printf("Processing order: %s for customer: %s", input.OrderID, input.CustomerID) + log.Printf("Full input: %+v", input) + + // Step 1: Validate Order (local activity) + var validationResult OrderValidationResult + if err := ctx.CallActivity("ValidateOrderActivity", + workflow.WithActivityInput(input)).Await(&validationResult); err != nil { + return nil, fmt.Errorf("order validation failed: %w", err) + } + + // Step 2: Process Payment (call Java payment service) + log.Println("=== STEP 2: Starting Payment Processing activity on payment-service ===") + var paymentResult string + if err := ctx.CallActivity("io.dapr.quickstarts.workflows.activities.ValidatePaymentMethodActivity", + workflow.WithActivityInput(input), + workflow.WithActivityAppID("payment-service")).Await(&paymentResult); err != nil { + log.Printf("ERROR: Payment processing failed with error: %v", err) + return nil, fmt.Errorf("payment processing failed: %w", err) + } + log.Println("=== STEP 2: Payment Processing COMPLETED ===") + + // Step 3: Reserve Inventory (call inventory-service) + log.Println("=== STEP 3: Starting Inventory Reservation activity on inventory-service ===") + var inventoryResult InventoryResult + if err := ctx.CallActivity("io.dapr.quickstarts.workflows.activities.ReserveInventoryActivity", + workflow.WithActivityInput(input), + workflow.WithActivityAppID("inventory-service")).Await(&inventoryResult); err != nil { + log.Printf("ERROR: Inventory reservation failed with error: %v", err) + return nil, fmt.Errorf("inventory reservation failed: %w", err) + } + log.Println("=== STEP 3: Inventory Reservation COMPLETED ===") + + // Step 4: Generate AI Recommendations (call AI recommendation service) + log.Println("=== STEP 4: Starting AI Recommendations activity on ai-recommendation-service ===") + var recommendationResult RecommendationResult + if err := ctx.CallActivity("io.dapr.quickstarts.workflows.activities.GeneratePersonalizedRecommendationsActivity", + workflow.WithActivityInput(input), + workflow.WithActivityAppID("ai-recommendation-service")).Await(&recommendationResult); err != nil { + log.Printf("ERROR: AI recommendations failed with error: %v", err) + return nil, fmt.Errorf("AI recommendations failed: %w", err) + } + log.Println("=== STEP 4: AI Recommendations COMPLETED ===") + + // Step 5: Complete Order (local activity) + log.Println("=== STEP 5: Starting Order Completion activity (local) ===") + var orderResult OrderResult + if err := ctx.CallActivity("CompleteOrderActivity", workflow.WithActivityInput(input)).Await(&orderResult); err != nil { + log.Printf("ERROR: Order completion failed with error: %v", err) + return nil, fmt.Errorf("order completion failed: %w", err) + } + log.Println("=== STEP 5: Order Completion COMPLETED ===") + + // Create final result with all working steps + finalResult := map[string]interface{}{ + "validation": validationResult, + "payment": paymentResult, + "inventory": inventoryResult, + "recommendations": recommendationResult, + "order": orderResult, + } + + log.Println("=== OrderProcessingWorkflow COMPLETED SUCCESSFULLY ===") + return finalResult, nil +} + +// ValidateOrderActivity validates the order locally +func ValidateOrderActivity(ctx workflow.ActivityContext) (any, error) { + log.Println("=== ValidateOrderActivity (local) STARTED ===") + + var input OrderRequest + if err := ctx.GetInput(&input); err != nil { + return nil, fmt.Errorf("failed to get activity input: %w", err) + } + log.Printf("Validating order: %s", input.OrderID) + + // Simulate validation logic + time.Sleep(1 * time.Second) + + result := OrderValidationResult{ + Valid: true, + Total: input.Total, + Message: "Order validation successful", + } + + log.Println("=== ValidateOrderActivity (local) COMPLETED ===") + return result, nil +} + +// CompleteOrderActivity completes the order locally +func CompleteOrderActivity(ctx workflow.ActivityContext) (any, error) { + log.Println("=== CompleteOrderActivity (local) STARTED ===") + + var input OrderRequest + if err := ctx.GetInput(&input); err != nil { + return nil, fmt.Errorf("failed to get activity input: %w", err) + } + log.Printf("Completing order: %s", input.OrderID) + + // Simulate order completion logic + time.Sleep(1 * time.Second) + + result := OrderResult{ + OrderID: input.OrderID, + CustomerID: input.CustomerID, + Status: "completed", + Total: input.Total, + Message: "Order completed successfully", + } + + log.Println("=== CompleteOrderActivity (local) COMPLETED ===") + return result, nil +} diff --git a/workflows/multiapp/go/order-orchestrator/models.go b/workflows/multiapp/go/order-orchestrator/models.go new file mode 100644 index 000000000..215102650 --- /dev/null +++ b/workflows/multiapp/go/order-orchestrator/models.go @@ -0,0 +1,66 @@ +package main + +// OrderRequest represents an incoming order +type OrderRequest struct { + OrderID string `json:"orderId"` + CustomerID string `json:"customerId"` + Items []Item `json:"items"` + PaymentMethod string `json:"paymentMethod"` + Total float64 `json:"total"` +} + +// CalculateTotal calculates the total price for all items in the order +func (o *OrderRequest) CalculateTotal() float64 { + total := 0.0 + for _, item := range o.Items { + total += item.Price * float64(item.Quantity) + } + o.Total = total + return total +} + +// Item represents a product in an order +type Item struct { + ProductID string `json:"productId"` + Name string `json:"name"` + Price float64 `json:"price"` + Quantity int `json:"quantity"` +} + +// OrderValidationResult represents the result of order validation +type OrderValidationResult struct { + Valid bool `json:"valid"` + Total float64 `json:"total"` + Message string `json:"message"` +} + +// InventoryResult represents the result of inventory reservation +type InventoryResult struct { + Success bool `json:"success"` + ReservedItems []Item `json:"reservedItems"` + Message string `json:"message"` +} + +// RecommendedItem represents a recommended product +type RecommendedItem struct { + ProductID string `json:"productId"` + Name string `json:"name"` + Price float64 `json:"price"` + Reason string `json:"reason"` +} + +// RecommendationResult represents the result of AI recommendations +type RecommendationResult struct { + Success bool `json:"success"` + Recommendations []RecommendedItem `json:"recommendations"` + Message string `json:"message"` +} + +// OrderResult represents the final result of order processing +type OrderResult struct { + OrderID string `json:"orderId"` + CustomerID string `json:"customerId"` + Status string `json:"status"` + Total float64 `json:"total"` + Message string `json:"message"` +} diff --git a/workflows/multiapp/java/.sdkmanrc b/workflows/multiapp/java/.sdkmanrc new file mode 100644 index 000000000..543d268ab --- /dev/null +++ b/workflows/multiapp/java/.sdkmanrc @@ -0,0 +1,4 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.11-tem +maven=3.8.5 \ No newline at end of file diff --git a/workflows/multiapp/java/ai-recommendation-service/pom.xml b/workflows/multiapp/java/ai-recommendation-service/pom.xml new file mode 100644 index 000000000..2c773818e --- /dev/null +++ b/workflows/multiapp/java/ai-recommendation-service/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + io.dapr.quickstarts + ai-recommendation-service + 1.0.0 + jar + + AI Recommendation Service + Java AI Recommendation Service for Multi-App Workflows + + + 17 + 17 + UTF-8 + 1.16.0 + + + + + + io.dapr + dapr-sdk-workflows + ${dapr.sdk.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + io.dapr.quickstarts.workflows.AIRecommendationServiceApplication + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/AIRecommendationServiceApplication.java b/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/AIRecommendationServiceApplication.java new file mode 100644 index 000000000..dd8c99ea6 --- /dev/null +++ b/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/AIRecommendationServiceApplication.java @@ -0,0 +1,51 @@ +package io.dapr.quickstarts.workflows; + +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.dapr.quickstarts.workflows.activities.GeneratePersonalizedRecommendationsActivity; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +/** + * AIRecommendationServiceWorker - registers only the GeneratePersonalizedRecommendationsActivity. + * This activity is called multi-app from the main workflow (written in Go). + */ +public class AIRecommendationServiceApplication { + + public static void main(String[] args) throws Exception { + System.out.println("=== Starting AIRecommendationServiceWorker (GeneratePersonalizedRecommendationsActivity) ==="); + + // Start HTTP server on port 50004 + HttpServer server = HttpServer.create(new InetSocketAddress(50004), 0); + server.createContext("/health", new HealthHandler()); + server.setExecutor(null); + server.start(); + System.out.println("HTTP server started on port 50004"); + + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder() + .registerActivity(GeneratePersonalizedRecommendationsActivity.class); + + // Build and start the workflow runtime + try (WorkflowRuntime runtime = builder.build()) { + System.out.println("AIRecommendationServiceWorker started - registered GeneratePersonalizedRecommendationsActivity only"); + System.out.println("AI Recommendation Service is ready to receive multi-app activity calls..."); + System.out.println("Waiting for multi-app activity calls..."); + runtime.start(); + } + } + + static class HealthHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String response = "AI Recommendation Service is running"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } +} diff --git a/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/activities/GeneratePersonalizedRecommendationsActivity.java b/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/activities/GeneratePersonalizedRecommendationsActivity.java new file mode 100644 index 000000000..1a636627c --- /dev/null +++ b/workflows/multiapp/java/ai-recommendation-service/src/main/java/io/dapr/quickstarts/workflows/activities/GeneratePersonalizedRecommendationsActivity.java @@ -0,0 +1,66 @@ +package io.dapr.quickstarts.workflows.activities; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; + +/** + * GeneratePersonalizedRecommendationsActivity for AI Recommendation Service - generates AI recommendations. + * This activity is called cross-app from the main workflow. + */ +public class GeneratePersonalizedRecommendationsActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext context) { + var logger = context.getLogger(); + logger.info("=== AI Recommendation Service: GeneratePersonalizedRecommendationsActivity STARTED ==="); + + try { + Object inputObj = context.getInput(Object.class); + logger.info("Received order: {}", inputObj); + + // Create RecommendationResult object + RecommendationResult result = new RecommendationResult(); + result.success = true; + result.recommendations = new RecommendedItem[]{ + new RecommendedItem("PROD-003", "Wireless Headphones", 99.99, "Based on your laptop purchase"), + new RecommendedItem("PROD-004", "USB-C Hub", 49.99, "Complements your laptop setup") + }; + result.message = "AI recommendations generated successfully by AI recommendation service"; + logger.info("=== AI Recommendation Service: GeneratePersonalizedRecommendationsActivity COMPLETED SUCCESSFULLY ==="); + + return result; + } catch (Exception e) { + logger.error("ERROR in GeneratePersonalizedRecommendationsActivity: {}", e.getMessage(), e); + throw e; + } + } + + // RecommendationResult class (to match Go struct) + public static class RecommendationResult { + public boolean success; + public RecommendedItem[] recommendations; + public String message; + + @Override + public String toString() { + return String.format("RecommendationResult{success=%s, recommendations=%d items, message='%s'}", + success, recommendations != null ? recommendations.length : 0, message); + } + } + + // RecommendedItem class (to match Go struct) + public static class RecommendedItem { + public String productId; + public String name; + public double price; + public String reason; + + public RecommendedItem() {} + + public RecommendedItem(String productId, String name, double price, String reason) { + this.productId = productId; + this.name = name; + this.price = price; + this.reason = reason; + } + } +} diff --git a/workflows/multiapp/java/inventory-service/pom.xml b/workflows/multiapp/java/inventory-service/pom.xml new file mode 100644 index 000000000..bf5401831 --- /dev/null +++ b/workflows/multiapp/java/inventory-service/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + io.dapr.quickstarts + inventory-service + 1.0.0 + jar + + Inventory Service + Java Inventory Service for Multi-App Workflows + + + 17 + 17 + UTF-8 + 1.16.0 + + + + + + io.dapr + dapr-sdk-workflows + ${dapr.sdk.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + io.dapr.quickstarts.workflows.InventoryServiceApplication + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + \ No newline at end of file diff --git a/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/InventoryServiceApplication.java b/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/InventoryServiceApplication.java new file mode 100644 index 000000000..80a5056ff --- /dev/null +++ b/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/InventoryServiceApplication.java @@ -0,0 +1,52 @@ +package io.dapr.quickstarts.workflows; + +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.dapr.quickstarts.workflows.activities.ReserveInventoryActivity; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +/** + * InventoryServiceWorker - registers only the ReserveInventoryActivity. + * This app will handle multi-app activity calls from the main workflow. + */ +public class InventoryServiceApplication { + + public static void main(String[] args) throws Exception { + System.out.println("=== Starting InventoryServiceWorker (ReserveInventoryActivity) ==="); + + // Start HTTP server on port 50003 + HttpServer server = HttpServer.create(new InetSocketAddress(50003), 0); + server.createContext("/health", new HealthHandler()); + server.setExecutor(null); + server.start(); + System.out.println("HTTP server started on port 50003"); + + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder() + .registerActivity(ReserveInventoryActivity.class); + + // Build and start the workflow runtime + try (WorkflowRuntime runtime = builder.build()) { + System.out.println("InventoryServiceWorker started - registered ReserveInventoryActivity only"); + System.out.println("Inventory Service is ready to receive cross-app activity calls..."); + System.out.println("Waiting for cross-app activity calls..."); + runtime.start(); + } + } + + static class HealthHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String response = "Inventory Service is running"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + +} diff --git a/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/activities/ReserveInventoryActivity.java b/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/activities/ReserveInventoryActivity.java new file mode 100644 index 000000000..676d6305e --- /dev/null +++ b/workflows/multiapp/java/inventory-service/src/main/java/io/dapr/quickstarts/workflows/activities/ReserveInventoryActivity.java @@ -0,0 +1,54 @@ +package io.dapr.quickstarts.workflows.activities; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; + +/** + * ReserveInventoryActivity for Inventory Service - reserves inventory items. + * This activity is called multi-app from the main workflow. + */ +public class ReserveInventoryActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext context) { + var logger = context.getLogger(); + logger.info("=== Inventory Service: ReserveInventoryActivity STARTED ==="); + + try { + Object inputObj = context.getInput(Object.class); + logger.info("Received order: {}", inputObj); + + // Create InventoryResult object + InventoryResult result = new InventoryResult(); + result.success = true; + result.reservedItems = new Item[0]; + result.message = "Inventory reserved successfully by inventory service"; + logger.info("=== Inventory Service: ReserveInventoryActivity COMPLETED SUCCESSFULLY ==="); + + return result; + } catch (Exception e) { + logger.error("ERROR in ReserveInventoryActivity: {}", e.getMessage(), e); + throw e; + } + } + + // InventoryResult class (to match Go struct) + public static class InventoryResult { + public boolean success; + public Item[] reservedItems; + public String message; + + @Override + public String toString() { + return String.format("InventoryResult{success=%s, reservedItems=%d items, message='%s'}", + success, reservedItems != null ? reservedItems.length : 0, message); + } + } + + // Item class (to match Go struct) + public static class Item { + public String productId; + public String name; + public double price; + public int quantity; + } +} diff --git a/workflows/multiapp/java/payment-service/pom.xml b/workflows/multiapp/java/payment-service/pom.xml new file mode 100644 index 000000000..ea9e880b5 --- /dev/null +++ b/workflows/multiapp/java/payment-service/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + io.dapr.quickstarts + payment-service + 1.0.0 + jar + + Payment Service + Java Payment Service for Multi-App Workflows + + + 17 + 17 + UTF-8 + 1.16.0 + + + + + + io.dapr + dapr-sdk-workflows + ${dapr.sdk.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + io.dapr.quickstarts.workflows.PaymentServiceApplication + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/PaymentServiceApplication.java b/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/PaymentServiceApplication.java new file mode 100644 index 000000000..b134fad3d --- /dev/null +++ b/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/PaymentServiceApplication.java @@ -0,0 +1,51 @@ +package io.dapr.quickstarts.workflows; + +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.dapr.quickstarts.workflows.activities.ValidatePaymentMethodActivity; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +/** + * PaymentServiceWorker - registers only the ValidatePaymentMethodActivity. + * This app will handle cross-app activity calls from the main workflow (written in Go). + */ +public class PaymentServiceApplication { + + public static void main(String[] args) throws Exception { + System.out.println("=== Starting PaymentServiceWorker (ValidatePaymentMethodActivity) ==="); + + // Start HTTP server on port 50002 + HttpServer server = HttpServer.create(new InetSocketAddress(50002), 0); + server.createContext("/health", new HealthHandler()); + server.setExecutor(null); + server.start(); + System.out.println("HTTP server started on port 50002"); + + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder() + .registerActivity(ValidatePaymentMethodActivity.class); + + // Build and start the workflow runtime + try (WorkflowRuntime runtime = builder.build()) { + System.out.println("PaymentServiceWorker started - registered ValidatePaymentMethodActivity only"); + System.out.println("Payment Service is ready to receive multi-app activity calls..."); + System.out.println("Waiting for multi-app activity calls..."); + runtime.start(); + } + } + + static class HealthHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String response = "Payment Service is running"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } +} diff --git a/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/activities/ValidatePaymentMethodActivity.java b/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/activities/ValidatePaymentMethodActivity.java new file mode 100644 index 000000000..9e295bec5 --- /dev/null +++ b/workflows/multiapp/java/payment-service/src/main/java/io/dapr/quickstarts/workflows/activities/ValidatePaymentMethodActivity.java @@ -0,0 +1,32 @@ +package io.dapr.quickstarts.workflows.activities; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; + +/** + * ValidatePaymentMethodActivity for Payment Service - validates payment methods. + * This activity is called cross-app from the main workflow (written in Go). + */ + public class ValidatePaymentMethodActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext context) { + var logger = context.getLogger(); + logger.info("=== Payment Service: ValidatePaymentMethodActivity STARTED ==="); + + try { + // Get the order input + Object inputObj = context.getInput(Object.class); + logger.info("Received order: {}", inputObj); + + // Simple payment validation logic + String result = "Payment validated successfully by payment service"; + + logger.info("=== Payment Service: ValidatePaymentMethodActivity COMPLETED SUCCESSFULLY ==="); + return result; + + } catch (Exception e) { + logger.error("ERROR in ValidatePaymentMethodActivity: {}", e.getMessage(), e); + throw e; + } + } + } \ No newline at end of file diff --git a/workflows/multiapp/makefile b/workflows/multiapp/makefile new file mode 100644 index 000000000..d2f95982c --- /dev/null +++ b/workflows/multiapp/makefile @@ -0,0 +1,84 @@ +# Multi-App E-commerce Workflow Demo +# This makefile helps you run the complete multi-app workflow scenario + +.PHONY: help build run-all run-go run-payment run-inventory run-ai clean + +# Default target +help: + @echo "Multi-App E-commerce Workflow Demo" + @echo "==================================" + @echo "" + @echo "Available targets:" + @echo " build - Build all services" + @echo " run-all - Run all services (requires 4 terminals)" + @echo " run-go - Run Go Order Orchestrator" + @echo " run-payment - Run Java Payment Service" + @echo " run-inventory - Run Java Inventory Service" + @echo " run-ai - Run Java AI Recommendation Service" + @echo " clean - Clean Java build artifacts" + @echo "" + +# Build all services +build: + @echo "Building Go Order Orchestrator..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd go && cd order-orchestrator && go mod tidy && go build -o order-orchestrator .' + @echo "Setting up Java environment..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env' + @echo "Building Java Payment Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd payment-service && mvn clean package -q' + @echo "Building Java Inventory Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd inventory-service && mvn clean package -q' + @echo "Building Java AI Recommendation Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd ai-recommendation-service && mvn clean package -q' + @echo "All services built successfully!" + +# Run all services (requires 4 terminals) +run-all: + @echo "Starting all services..." + @echo "Please open 4 terminals and run:" + @echo " Terminal 1: make run-go" + @echo " Terminal 2: make run-payment" + @echo " Terminal 3: make run-inventory" + @echo " Terminal 4: make run-ai" + @echo "" + @echo "Or run them individually:" + @echo " make run-go" + @echo " make run-payment" + @echo " make run-inventory" + @echo " make run-ai" + +# Run Go Order Orchestrator +run-go: + @echo "Starting Go Order Orchestrator..." + cd go/order-orchestrator && dapr run --app-id order-orchestrator --app-port 50001 --dapr-http-port 3505 --dapr-grpc-port 50014 --resources-path ../../components -- go run . + +# Run Java Payment Service +run-payment: + @echo "Starting Java Payment Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd payment-service && dapr run --app-id payment-service --app-port 50002 --dapr-http-port 3506 --dapr-grpc-port 50015 --resources-path ../../components -- java -jar target/payment-service-1.0.0.jar' + +# Run Java Inventory Service +run-inventory: + @echo "Starting Java Inventory Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd inventory-service && dapr run --app-id inventory-service --app-port 50003 --dapr-http-port 3507 --dapr-grpc-port 50016 --resources-path ../../components -- java -jar target/inventory-service-1.0.0.jar' + +# Run Java AI Recommendation Service +run-ai: + @echo "Starting Java AI Recommendation Service..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd ai-recommendation-service && dapr run --app-id ai-recommendation-service --app-port 50004 --dapr-http-port 3508 --dapr-grpc-port 50017 --resources-path ../../components -- java -jar target/ai-recommendation-service-1.0.0.jar' + +# Clean Java build artifacts +clean: + @echo "Cleaning Java build artifacts..." + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd payment-service && mvn clean compile -q' + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd inventory-service && mvn clean compile -q' + @bash -c 'source ~/.sdkman/bin/sdkman-init.sh && cd java && sdk env && cd ai-recommendation-service && mvn clean compile -q' + @echo "Clean completed!" + +# Test the workflow +test: + @echo "Testing the multi-app workflow..." + @echo "Sending POST request to: http://localhost:50001/start-workflow" + @curl -X POST http://localhost:50001/start-workflow + @echo "" + @echo "Test completed!"