├── .editorconfig ├── .github └── workflows │ └── validate-build.yml ├── .gitignore ├── DurableTask.Dapr.sln ├── LICENSE ├── README.md ├── nuget.config ├── samples └── order-processing-service │ ├── components │ ├── pubsub.yaml │ └── statestore.yaml │ ├── demo.http │ ├── inventory-service │ ├── README.md │ ├── app.py │ └── requirements.txt │ ├── notification-service │ ├── README.md │ ├── app.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json │ ├── payments-service │ ├── README.md │ ├── app.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json │ └── web-api │ ├── OrderingWebApi.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ └── README.md ├── src ├── Dapr.Workflow │ ├── Dapr.Workflow.csproj │ ├── InvokeServiceMethodActivity.cs │ ├── WorkflowClient.cs │ ├── WorkflowContext.cs │ ├── WorkflowMetadata.cs │ ├── WorkflowRuntimeOptions.cs │ └── WorkflowServiceCollectionExtensions.cs ├── DaprOrchestrationService.cs ├── DurableTask.Dapr │ ├── Activities │ │ ├── ActivityCompletionResponse.cs │ │ ├── ActivityInvocationRequest.cs │ │ ├── IActivityActor.cs │ │ ├── IActivityExecutor.cs │ │ └── StatelessActivityActor.cs │ ├── DaprOptions.cs │ ├── DaprOrchestrationService.cs │ ├── DurableTask.Dapr.csproj │ ├── Helpers.cs │ ├── Logs.cs │ ├── OrchestrationServiceBase.cs │ ├── ReliableActor.cs │ └── Workflows │ │ ├── IWorkflowActor.cs │ │ ├── IWorkflowExecutor.cs │ │ └── WorkflowActor.cs └── Workflows │ ├── IWorkflowActor.cs │ ├── IWorkflowExecutor.cs │ └── WorkflowActor.cs └── test ├── DurableTask.Dapr.Tests └── DurableTask.Dapr.Tests.csproj └── ManualTesting ├── ManualTesting.csproj ├── Program.cs └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = true 23 | 24 | # this. and Me. preferences 25 | dotnet_style_qualification_for_event = true:warning 26 | dotnet_style_qualification_for_field = true:warning 27 | dotnet_style_qualification_for_method = true:warning 28 | dotnet_style_qualification_for_property = true:warning 29 | 30 | # Language keywords vs BCL types preferences 31 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 32 | dotnet_style_predefined_type_for_member_access = true:silent 33 | 34 | # Parentheses preferences 35 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | 40 | # Modifier preferences 41 | dotnet_style_require_accessibility_modifiers = omit_if_default:warning 42 | 43 | # Expression-level preferences 44 | dotnet_style_coalesce_expression = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_explicit_tuple_names = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_object_initializer = true:suggestion 49 | dotnet_style_prefer_auto_properties = true:silent 50 | dotnet_style_prefer_compound_assignment = true:suggestion 51 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 52 | dotnet_style_prefer_conditional_expression_over_return = true:silent 53 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 54 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 55 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 56 | dotnet_style_prefer_simplified_interpolation = true:suggestion 57 | 58 | # Field preferences 59 | dotnet_style_readonly_field = true:warning 60 | 61 | # Parameter preferences 62 | dotnet_code_quality_unused_parameters = all:suggestion 63 | 64 | #### C# Coding Conventions #### 65 | 66 | # var preferences 67 | csharp_style_var_elsewhere = false:suggestion 68 | csharp_style_var_for_built_in_types = false:suggestion 69 | csharp_style_var_when_type_is_apparent = false:silent 70 | 71 | # Expression-bodied members 72 | csharp_style_expression_bodied_accessors = true:silent 73 | csharp_style_expression_bodied_constructors = false:silent 74 | csharp_style_expression_bodied_indexers = true:silent 75 | csharp_style_expression_bodied_lambdas = true:silent 76 | csharp_style_expression_bodied_local_functions = false:silent 77 | csharp_style_expression_bodied_methods = false:silent 78 | csharp_style_expression_bodied_operators = false:silent 79 | csharp_style_expression_bodied_properties = true:silent 80 | 81 | # Pattern matching preferences 82 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 83 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 84 | csharp_style_prefer_switch_expression = false:suggestion 85 | 86 | # Null-checking preferences 87 | csharp_style_conditional_delegate_call = true:suggestion 88 | 89 | # Modifier preferences 90 | csharp_prefer_static_local_function = true:suggestion 91 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 92 | 93 | # Code-block preferences 94 | csharp_prefer_braces = true:suggestion 95 | csharp_prefer_simple_using_statement = true:suggestion 96 | 97 | # Expression-level preferences 98 | csharp_prefer_simple_default_expression = true:suggestion 99 | csharp_style_deconstructed_variable_declaration = true:suggestion 100 | csharp_style_inlined_variable_declaration = true:suggestion 101 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 102 | csharp_style_prefer_index_operator = true:suggestion 103 | csharp_style_prefer_range_operator = true:suggestion 104 | csharp_style_throw_expression = true:suggestion 105 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 106 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 107 | 108 | # 'using' directive preferences 109 | csharp_using_directive_placement = outside_namespace:silent 110 | 111 | #### C# Formatting Rules #### 112 | 113 | # New line preferences 114 | csharp_new_line_before_catch = true 115 | csharp_new_line_before_else = true 116 | csharp_new_line_before_finally = true 117 | csharp_new_line_before_members_in_anonymous_types = true 118 | csharp_new_line_before_members_in_object_initializers = true 119 | csharp_new_line_before_open_brace = all 120 | csharp_new_line_between_query_expression_clauses = true 121 | 122 | # Indentation preferences 123 | csharp_indent_block_contents = true 124 | csharp_indent_braces = false 125 | csharp_indent_case_contents = true 126 | csharp_indent_case_contents_when_block = true 127 | csharp_indent_labels = one_less_than_current 128 | csharp_indent_switch_labels = true 129 | 130 | # Space preferences 131 | csharp_space_after_cast = false 132 | csharp_space_after_colon_in_inheritance_clause = true 133 | csharp_space_after_comma = true 134 | csharp_space_after_dot = false 135 | csharp_space_after_keywords_in_control_flow_statements = true 136 | csharp_space_after_semicolon_in_for_statement = true 137 | csharp_space_around_binary_operators = before_and_after 138 | csharp_space_around_declaration_statements = false 139 | csharp_space_before_colon_in_inheritance_clause = true 140 | csharp_space_before_comma = false 141 | csharp_space_before_dot = false 142 | csharp_space_before_open_square_brackets = false 143 | csharp_space_before_semicolon_in_for_statement = false 144 | csharp_space_between_empty_square_brackets = false 145 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 146 | csharp_space_between_method_call_name_and_opening_parenthesis = false 147 | csharp_space_between_method_call_parameter_list_parentheses = false 148 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 149 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 150 | csharp_space_between_method_declaration_parameter_list_parentheses = false 151 | csharp_space_between_parentheses = false 152 | csharp_space_between_square_brackets = false 153 | 154 | # Wrapping preferences 155 | csharp_preserve_single_line_blocks = true 156 | csharp_preserve_single_line_statements = true 157 | 158 | #### Naming styles #### 159 | 160 | # Naming rules 161 | 162 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 163 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 164 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 165 | 166 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 167 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 168 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 169 | 170 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 171 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 172 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 173 | 174 | # Symbol specifications 175 | 176 | dotnet_naming_symbols.interface.applicable_kinds = interface 177 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 178 | dotnet_naming_symbols.interface.required_modifiers = 179 | 180 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 181 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 182 | dotnet_naming_symbols.types.required_modifiers = 183 | 184 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 185 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 186 | dotnet_naming_symbols.non_field_members.required_modifiers = 187 | 188 | # Naming styles 189 | 190 | dotnet_naming_style.pascal_case.required_prefix = 191 | dotnet_naming_style.pascal_case.required_suffix = 192 | dotnet_naming_style.pascal_case.word_separator = 193 | dotnet_naming_style.pascal_case.capitalization = pascal_case 194 | 195 | dotnet_naming_style.begins_with_i.required_prefix = I 196 | dotnet_naming_style.begins_with_i.required_suffix = 197 | dotnet_naming_style.begins_with_i.word_separator = 198 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 199 | 200 | # Other 201 | 202 | file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. 203 | csharp_style_namespace_declarations = file_scoped:silent 204 | csharp_style_prefer_method_group_conversion = true:silent 205 | csharp_style_prefer_null_check_over_type_check = true:suggestion 206 | 207 | [*.{cs,vb}] 208 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 209 | tab_width = 4 210 | indent_size = 4 211 | end_of_line = crlf 212 | dotnet_style_coalesce_expression = true:suggestion 213 | dotnet_style_null_propagation = true:suggestion 214 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 215 | dotnet_style_prefer_auto_properties = true:silent 216 | dotnet_style_object_initializer = true:suggestion 217 | dotnet_style_collection_initializer = true:suggestion 218 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 219 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 220 | dotnet_style_prefer_conditional_expression_over_return = true:silent 221 | dotnet_style_explicit_tuple_names = true:suggestion 222 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 223 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 224 | dotnet_style_prefer_compound_assignment = true:suggestion 225 | dotnet_style_prefer_simplified_interpolation = true:suggestion 226 | dotnet_style_namespace_match_folder = true:suggestion 227 | 228 | # https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview 229 | guidelines = 120 230 | -------------------------------------------------------------------------------- /.github/workflows/validate-build.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v2 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /DurableTask.Dapr.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32126.317 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5D8C7CC1-3734-460E-B19F-234D9FE8B8AE}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | nuget.config = nuget.config 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.Dapr", "src\DurableTask.Dapr\DurableTask.Dapr.csproj", "{0CC77842-4679-413A-8151-1611885F7504}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1019D639-F84B-4218-BA87-5F0EFCA783F8}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "order-processing-service", "order-processing-service", "{285E7F62-224D-4D70-AB9B-CFCA615AC5CA}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "notification-service", "notification-service", "{C6D345B2-DE6A-42AF-A5CC-E0EE35275305}" 20 | ProjectSection(SolutionItems) = preProject 21 | samples\order-processing-service\notification-service\app.ts = samples\order-processing-service\notification-service\app.ts 22 | samples\order-processing-service\notification-service\index.js = samples\order-processing-service\notification-service\index.js 23 | samples\order-processing-service\notification-service\package.json = samples\order-processing-service\notification-service\package.json 24 | EndProjectSection 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{48A0D675-C294-4D76-BA3B-BD89961EAF8B}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManualTesting", "test\ManualTesting\ManualTesting.csproj", "{3462245F-51CB-4B4F-B41C-309F1DDC7A2B}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.Dapr.Tests", "test\DurableTask.Dapr.Tests\DurableTask.Dapr.Tests.csproj", "{091F3C29-856E-4E2F-9E4A-59EBC6965C86}" 31 | EndProject 32 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "web-api", "web-api", "{76E9C700-415B-47EA-B665-1EB110B551EE}" 33 | EndProject 34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrderingWebApi", "samples\order-processing-service\web-api\OrderingWebApi.csproj", "{644F7359-4C18-4193-8AA2-BB6BDED6BF95}" 35 | EndProject 36 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow", "src\Dapr.Workflow\Dapr.Workflow.csproj", "{E293AE6D-1C1A-4E3C-8CD6-0BD0C8A798D8}" 37 | EndProject 38 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", "{88AC5DA7-EBD1-47EF-A70E-9302FC3FCEB5}" 39 | ProjectSection(SolutionItems) = preProject 40 | samples\order-processing-service\components\pubsub.yaml = samples\order-processing-service\components\pubsub.yaml 41 | samples\order-processing-service\components\statestore.yaml = samples\order-processing-service\components\statestore.yaml 42 | EndProjectSection 43 | EndProject 44 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "payments-service", "payments-service", "{2FADA62F-F8A2-4EA1-B35A-CB8A570A6062}" 45 | ProjectSection(SolutionItems) = preProject 46 | samples\order-processing-service\payments-service\app.ts = samples\order-processing-service\payments-service\app.ts 47 | samples\order-processing-service\payments-service\package.json = samples\order-processing-service\payments-service\package.json 48 | samples\order-processing-service\payments-service\README.md = samples\order-processing-service\payments-service\README.md 49 | EndProjectSection 50 | EndProject 51 | Global 52 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 53 | Debug|Any CPU = Debug|Any CPU 54 | Release|Any CPU = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 57 | {0CC77842-4679-413A-8151-1611885F7504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {0CC77842-4679-413A-8151-1611885F7504}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {0CC77842-4679-413A-8151-1611885F7504}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {0CC77842-4679-413A-8151-1611885F7504}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {3462245F-51CB-4B4F-B41C-309F1DDC7A2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {3462245F-51CB-4B4F-B41C-309F1DDC7A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {3462245F-51CB-4B4F-B41C-309F1DDC7A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {3462245F-51CB-4B4F-B41C-309F1DDC7A2B}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {091F3C29-856E-4E2F-9E4A-59EBC6965C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {091F3C29-856E-4E2F-9E4A-59EBC6965C86}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {091F3C29-856E-4E2F-9E4A-59EBC6965C86}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {091F3C29-856E-4E2F-9E4A-59EBC6965C86}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {644F7359-4C18-4193-8AA2-BB6BDED6BF95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {644F7359-4C18-4193-8AA2-BB6BDED6BF95}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {644F7359-4C18-4193-8AA2-BB6BDED6BF95}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {644F7359-4C18-4193-8AA2-BB6BDED6BF95}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {E293AE6D-1C1A-4E3C-8CD6-0BD0C8A798D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {E293AE6D-1C1A-4E3C-8CD6-0BD0C8A798D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {E293AE6D-1C1A-4E3C-8CD6-0BD0C8A798D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {E293AE6D-1C1A-4E3C-8CD6-0BD0C8A798D8}.Release|Any CPU.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(SolutionProperties) = preSolution 79 | HideSolutionNode = FALSE 80 | EndGlobalSection 81 | GlobalSection(NestedProjects) = preSolution 82 | {285E7F62-224D-4D70-AB9B-CFCA615AC5CA} = {1019D639-F84B-4218-BA87-5F0EFCA783F8} 83 | {C6D345B2-DE6A-42AF-A5CC-E0EE35275305} = {285E7F62-224D-4D70-AB9B-CFCA615AC5CA} 84 | {3462245F-51CB-4B4F-B41C-309F1DDC7A2B} = {48A0D675-C294-4D76-BA3B-BD89961EAF8B} 85 | {091F3C29-856E-4E2F-9E4A-59EBC6965C86} = {48A0D675-C294-4D76-BA3B-BD89961EAF8B} 86 | {76E9C700-415B-47EA-B665-1EB110B551EE} = {285E7F62-224D-4D70-AB9B-CFCA615AC5CA} 87 | {644F7359-4C18-4193-8AA2-BB6BDED6BF95} = {76E9C700-415B-47EA-B665-1EB110B551EE} 88 | {88AC5DA7-EBD1-47EF-A70E-9302FC3FCEB5} = {285E7F62-224D-4D70-AB9B-CFCA615AC5CA} 89 | {2FADA62F-F8A2-4EA1-B35A-CB8A570A6062} = {285E7F62-224D-4D70-AB9B-CFCA615AC5CA} 90 | EndGlobalSection 91 | GlobalSection(ExtensibilityGlobals) = postSolution 92 | SolutionGuid = {C48C815F-B07A-4E95-9BE9-192DB216D52C} 93 | EndGlobalSection 94 | EndGlobal 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Chris Gillum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dapr Provider for the Durable Task Framework 2 | 3 | This repo contains the code for a [Durable Task Framework](https://github.com/Azure/durabletask) .NET-based storage provider that uses [Dapr Actors](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/) for state storage and scheduling. It's meant to be used as a proof-of-concept / reference implementation for the embedded engine described in the [Dapr Workflow proposal](https://github.com/dapr/dapr/issues/4576). 4 | 5 | This repo also contains a sample Dapr Workflow SDK for .NET that is designed to be used with the Dapr backend. 6 | 7 | > **IMPORTANT** This code is NOT going to be used in any way as the actual Dapr Workflow implementation. The "real" implementation will be done in Go so that it can be embedded into the Dapr sidecar. This project is just a reference implementation to help us tackle some of the design details around running workflows on top of actors. 8 | 9 | ## Getting started 10 | 11 | If you're interested in playing with this reference implementation, please see the README and the code under [test/DaprTesting](test/DaprTesting/). Alternatively, you can find an end-to-end sample under [samples](samples). 12 | 13 | ### Prerequisites 14 | 15 | The following dependencies must be installed on your machine to build and test this reference implementation. 16 | 17 | * [.NET 6 SDK or newer](https://dotnet.microsoft.com/download/dotnet/6.0) 18 | * [Dapr CLI 1.8 or newer](https://docs.dapr.io/getting-started/install-dapr-cli/) 19 | 20 | Other dependencies may be required for building and running the samples. 21 | 22 | ## Contributing 23 | 24 | Contributions via PRs are absolutely welcome! Keep in mind, however, that this is a reference implementation and not meant to be used for actual application development. A repo in the [Dapr GitHub organization](https://github.com/dapr) will be used for the real implementation. 25 | 26 | Feel free to open issues in this repo to ask questions or provide feedback about the reference implementation. If you have questions or feedback about Dapr Workflow generally, please submit comments to https://github.com/dapr/dapr/issues/4576. 27 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/order-processing-service/components/pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: notifications-pubsub 5 | spec: 6 | type: pubsub.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: localhost:6379 11 | - name: redisPassword 12 | value: "" 13 | -------------------------------------------------------------------------------- /samples/order-processing-service/components/statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: localhost:6379 11 | - name: redisPassword 12 | value: "" 13 | - name: actorStateStore 14 | value: "true" 15 | -------------------------------------------------------------------------------- /samples/order-processing-service/demo.http: -------------------------------------------------------------------------------- 1 | ### Create new order (no approval required) 2 | POST http://localhost:8080/orders 3 | Content-Type: application/json 4 | 5 | { "name": "catfood", "quantity": 3, "totalCost": 19.99 } 6 | 7 | ### Query placeholder 8 | GET http://localhost:8080/orders/XXX 9 | 10 | 11 | ### Create new order (approval required) 12 | POST http://localhost:8080/orders 13 | Content-Type: application/json 14 | 15 | { "name": "iphone", "quantity": 1, "totalCost": 1199.99 } 16 | 17 | ### Approve placeholder 18 | POST http://localhost:8080/orders/XXX/approve 19 | 20 | ### Create new order (durability test) 21 | POST http://localhost:8080/orders 22 | Content-Type: application/json 23 | 24 | { "name": "tacos", "quantity": 3, "totalCost": 4.99 } 25 | 26 | 27 | ### Existing workflow - Tesla order 28 | GET http://localhost:8080/orders/9cbf66c4 29 | 30 | ### Existing workflow - BMW order 31 | GET http://localhost:8080/orders/f6142363 -------------------------------------------------------------------------------- /samples/order-processing-service/inventory-service/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a Python Flask app that represents an inventory service. It's called by the order processing workflow when checking to see if there is sufficient inventory for a particular order. The workflow uses the Dapr service invocation building block to invoke this service. 4 | 5 | ## Prerequisites 6 | 7 | * [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) 8 | * [Python 3.7+ installed](https://www.python.org/downloads/) 9 | * `PATH` environment variable includes `python` command and maps to Python 3. 10 | 11 | ## Install dependencies 12 | 13 | This will install Flask as well as the Dapr SDK for Python. 14 | 15 | ```bash 16 | pip3 install -r requirements.txt 17 | ``` 18 | 19 | ## Running the service 20 | 21 | This will start up both the Dapr sidecar process and the Python app together. 22 | 23 | ```bash 24 | dapr run --app-id inventory --app-port 5006 --dapr-http-port 3506 -- python app.py 25 | ``` 26 | -------------------------------------------------------------------------------- /samples/order-processing-service/inventory-service/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import json 3 | import os 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route('/reserve-inventory', methods=['POST']) 9 | def reserve_inventory(): 10 | data = request.json 11 | print('Request received : ' + json.dumps(data), flush=True) 12 | return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} 13 | 14 | 15 | app.run(port=os.environ.get('APP_PORT', 5006)) -------------------------------------------------------------------------------- /samples/order-processing-service/inventory-service/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | dapr -------------------------------------------------------------------------------- /samples/order-processing-service/notification-service/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a Node.js app that represents the notification service. It's invoked as a pub/sub subscriber and is triggered several times by the order processing workflow as it makes progress. 4 | 5 | Note that the pub/sub configuration can be found in [pubsub.yaml](../components/pubsub.yaml). 6 | 7 | ## Prerequisites 8 | 9 | * [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) 10 | * [Latest Node.js installed](https://nodejs.org/) 11 | * [ts-node](https://www.npmjs.com/package/ts-node) 12 | * Make sure `npm` and `ts-node` are in the `PATH` 13 | 14 | ## Install dependencies 15 | 16 | This will install TypeScript, express.js, and the Dapr SDK for JavaScript. 17 | 18 | ```bash 19 | npm install 20 | ``` 21 | 22 | ## Running the service 23 | 24 | This will start up both the Dapr sidecar process and the Node.js app together. 25 | 26 | ```bash 27 | npm run start:dapr 28 | ``` 29 | -------------------------------------------------------------------------------- /samples/order-processing-service/notification-service/app.ts: -------------------------------------------------------------------------------- 1 | import { DaprServer, CommunicationProtocolEnum } from 'dapr-client'; 2 | 3 | const DAPR_HOST = process.env.DAPR_HOST || "http://localhost"; 4 | const DAPR_HTTP_PORT = process.env.DAPR_HTTP_PORT || "3502"; 5 | const SERVER_HOST = process.env.SERVER_HOST || "127.0.0.1"; 6 | const SERVER_PORT = process.env.SERVER_PORT || "5002"; 7 | const PUBSUB_NAME = process.env.PUBSUB_NAME || "notifications-pubsub"; 8 | const PUBSUB_TOPIC = process.env.PUBSUB_TOPIC || "notifications"; 9 | 10 | async function main() { 11 | const server = new DaprServer( 12 | SERVER_HOST, 13 | SERVER_PORT, 14 | DAPR_HOST, 15 | DAPR_HTTP_PORT, 16 | CommunicationProtocolEnum.HTTP); 17 | 18 | console.log(`Listening for subscriptions on ${SERVER_HOST}:${SERVER_PORT}. Pubsub name: '${PUBSUB_NAME}'. Topic: '${PUBSUB_TOPIC}'.`) 19 | 20 | await server.pubsub.subscribe(PUBSUB_NAME, PUBSUB_TOPIC, async (data) => { 21 | console.log("Notification received: " + JSON.stringify(data)); 22 | }); 23 | 24 | await server.start(); 25 | }; 26 | 27 | main().catch(e => console.error(e)); -------------------------------------------------------------------------------- /samples/order-processing-service/notification-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ts-node app.ts", 7 | "start:dapr": "dapr run --app-id notifications --app-protocol http --dapr-http-port 3502 --app-port 5002 --components-path ../components -- ts-node app.ts" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "typescript": "^4.7.4" 14 | }, 15 | "dependencies": { 16 | "dapr-client": "^2.3.0", 17 | "express": "^4.17.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/order-processing-service/notification-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": [ 11 | "es2015" 12 | ] 13 | } -------------------------------------------------------------------------------- /samples/order-processing-service/payments-service/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a Node.js app that represents the payments service. It's called by the order processing workflow when completing the order process. The workflow uses the Dapr service invocation building block to invoke this service. 4 | 5 | ## Prerequisites 6 | 7 | * [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) 8 | * [Latest Node.js installed](https://nodejs.org/) 9 | * [ts-node](https://www.npmjs.com/package/ts-node) 10 | * Make sure `npm` and `ts-node` are in the `PATH` 11 | 12 | ## Install dependencies 13 | 14 | This will install TypeScript and express.js. 15 | 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | ## Running the service 21 | 22 | This will start up both the Dapr sidecar process and the Node.js app together. 23 | 24 | ```bash 25 | npm run start:dapr 26 | ``` 27 | -------------------------------------------------------------------------------- /samples/order-processing-service/payments-service/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const port = 5004; 4 | 5 | const app = express(); 6 | app.use(express.json()); 7 | 8 | app.post('/process-payment', (req, res) => { 9 | console.log(`Payment received: ${req.body.amount} ${req.body.currency}`) 10 | res.send({'message':'payment received!'}); 11 | }); 12 | 13 | app.listen(port, () => { 14 | return console.log(`Payment Service is listening at http://localhost:${port}`); 15 | }); -------------------------------------------------------------------------------- /samples/order-processing-service/payments-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payments-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ts-node app.ts", 7 | "start:dapr": "dapr run --app-id payments --app-protocol http --dapr-http-port 3504 --app-port 5004 --components-path ../components -- ts-node app.ts" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/express": "^4.17.1", 14 | "typescript": "^4.7.4" 15 | }, 16 | "dependencies": { 17 | "express": "^4.17.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/order-processing-service/payments-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": ["es2015"] 11 | } -------------------------------------------------------------------------------- /samples/order-processing-service/web-api/OrderingWebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/order-processing-service/web-api/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json.Serialization; 5 | using Dapr.Workflow; 6 | using Microsoft.AspNetCore.Mvc; 7 | using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; 8 | 9 | // The workflow host is a background service that connects to the sidecar over gRPC 10 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 11 | 12 | // Add workflows. NOTE: This could alternatively be placed in another project. 13 | builder.Services.AddWorkflow(options => 14 | { 15 | // Inline order processing workflow logic 16 | options.RegisterWorkflow("ProcessOrder", implementation: async (context, input) => 17 | { 18 | ArgumentNullException.ThrowIfNull(input, nameof(input)); 19 | 20 | // Notify order received 21 | context.PublishEvent( 22 | pubSubName: "notifications-pubsub", 23 | topic: "notifications", 24 | payload: $"Received order {context.InstanceId} for {input.Name} at {input.TotalCost:c}"); 25 | 26 | // Invoke the inventory service to reserve the specified items 27 | InventoryResult? result = await context.InvokeMethodAsync( 28 | httpMethod: HttpMethod.Post, 29 | appId: "inventory", 30 | methodName: "reserve-inventory", 31 | data: new { item = input.Name, quantity = input.Quantity }); 32 | 33 | if (result?.success != true) 34 | { 35 | context.SetCustomStatus($"Insufficient inventory for {input.Name}"); 36 | return new OrderResult(Processed: false); 37 | } 38 | 39 | // Orders >= $1,000 require an approval to be received within 24 hours 40 | if (input.TotalCost >= 1000.00) 41 | { 42 | // Notify waiting for approval 43 | TimeSpan approvalTimeout = TimeSpan.FromHours(24); 44 | string message = $"Waiting for approval. Deadline = {context.CurrentUtcDateTime.Add(approvalTimeout):s}"; 45 | context.SetCustomStatus(message); 46 | context.PublishEvent(pubSubName: "notifications-pubsub", topic: "notifications", payload: message); 47 | 48 | try 49 | { 50 | // Wait up to 24-hours for an "Approval" event to be delivered to this workflow instance 51 | await context.WaitForExternalEventAsync("Approval", approvalTimeout); 52 | } 53 | catch (TaskCanceledException) 54 | { 55 | // Notify approval deadline expired 56 | context.PublishEvent( 57 | pubSubName: "notifications-pubsub", 58 | topic: "notifications", 59 | payload: $"Approval deadline for order {context.InstanceId} expired!"); 60 | 61 | return new OrderResult(Processed: false); 62 | } 63 | 64 | // Notify approval received 65 | message = $"Received approval at {context.CurrentUtcDateTime:s}"; 66 | context.SetCustomStatus(message); 67 | context.PublishEvent(pubSubName: "notifications-pubsub", topic: "notifications", payload: message); 68 | } 69 | 70 | // Invoke the payment service 71 | await context.InvokeMethodAsync( 72 | httpMethod: HttpMethod.Post, 73 | appId: "payments", 74 | methodName: "process-payment", 75 | data: new { amount = input.TotalCost, currency = "USD" }); 76 | 77 | // Notify order processed successfully 78 | context.PublishEvent( 79 | pubSubName: "notifications-pubsub", 80 | topic: "notifications", 81 | payload: $"Order {context.InstanceId} was processed successfully!"); 82 | 83 | return new OrderResult(Processed: true); 84 | }); 85 | }); 86 | 87 | // Configure JSON options. 88 | builder.Services.Configure(options => 89 | { 90 | options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); 91 | options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 92 | }); 93 | 94 | WebApplication app = builder.Build(); 95 | 96 | // POST creates new orders 97 | app.MapPost("/orders", async (WorkflowClient client, LinkGenerator linker, [FromBody] OrderPayload? orderInput) => 98 | { 99 | if (orderInput == null || orderInput.Name == null) 100 | { 101 | return Results.BadRequest(new 102 | { 103 | message = "Order data was missing from the request", 104 | example = new OrderPayload("Paperclips", 99.95), 105 | }); 106 | } 107 | 108 | // TODO: Add some content 109 | string orderId = Guid.NewGuid().ToString()[..8]; 110 | await client.ScheduleNewWorkflowAsync("ProcessOrder", orderId, orderInput); 111 | return Results.AcceptedAtRoute("GetOrder", new { orderId }, new 112 | { 113 | id = orderId, 114 | orderInput, 115 | }); 116 | }).WithName("CreateOrder"); 117 | 118 | // GET returns the status of existing orders 119 | app.MapGet("/orders/{orderId}", async (string orderId, WorkflowClient client) => 120 | { 121 | WorkflowMetadata metadata = await client.GetWorkflowMetadata(orderId, getInputsAndOutputs: true); 122 | if (metadata.Exists) 123 | { 124 | return Results.Ok(metadata); 125 | } 126 | else 127 | { 128 | return Results.NotFound($"No order with ID = '{orderId}' was found."); 129 | } 130 | }).WithName("GetOrder"); 131 | 132 | // POST to submit a manual approval to the order workflow 133 | app.MapPost("/orders/{orderId}/approve", async (string orderId, WorkflowClient client) => 134 | { 135 | WorkflowMetadata metadata = await client.GetWorkflowMetadata(orderId, getInputsAndOutputs: true); 136 | if (!metadata.Exists) 137 | { 138 | return Results.NotFound($"No order with ID = '{orderId}' was found."); 139 | } 140 | else if (!metadata.IsWorkflowRunning) 141 | { 142 | return Results.BadRequest($"This order has already completed processing."); 143 | } 144 | 145 | // Raise the approval event to the running workflow instance 146 | await client.RaiseEventAsync(orderId, "Approval"); 147 | 148 | return Results.Accepted(); 149 | }).WithName("ApproveOrder"); 150 | 151 | // Start the web server 152 | app.Run("http://0.0.0.0:8080"); 153 | 154 | record OrderPayload(string Name, double TotalCost, int Quantity = 1); 155 | record OrderResult(bool Processed); 156 | record InventoryResult(bool success); 157 | -------------------------------------------------------------------------------- /samples/order-processing-service/web-api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "OrderingWebApi": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:57899;http://localhost:57900" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/order-processing-service/web-api/README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | * [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) 4 | * [Docker Desktop](https://www.docker.com/products/docker-desktop) 5 | 6 | ## Building and running 7 | 8 | Use the following commands to build the project. 9 | 10 | ```bash 11 | dotnet build 12 | ``` 13 | 14 | You can run the project with the following command: 15 | 16 | ```bash 17 | dotnet run 18 | ``` 19 | 20 | ## Durable Task Sidecar with Dapr Workflow support 21 | 22 | This starts the Durable Task sidecar which is preconfigured with the latest DurableTask.Dapr backend. 23 | This process is where the workflow actor code lives, and interacts directly with the Dapr sidecar and assumes 24 | that the gRPC endpoint is on port 50001. 25 | The code in the web-api project will connect to this sidecar on port 4001. 26 | 27 | ```bash 28 | docker run --name durabletask-sidecar-dapr -p 4001:4001 -d cgillum/durabletask-sidecar:0.3.3-dapr --backend Dapr 29 | ``` 30 | 31 | ## Dapr CLI 32 | 33 | This starts the Dapr sidecar in standalone mode. It does *not* attempt to start the web API / workflow app. 34 | 35 | ```bash 36 | dapr run --app-id web-api --app-port 5000 --dapr-http-port 3500 --dapr-grpc-port 50001 --components-path ../components 37 | ``` 38 | 39 | If you want to run the Dapr sidecar together with the app, run the following command instead: 40 | 41 | ```bash 42 | dapr run --app-id web-api --app-port 5000 --dapr-http-port 3500 --dapr-grpc-port 50001 --components-path ../components -- dotnet run 43 | ``` 44 | 45 | ## Examples 46 | 47 | See the [demo.http](../demo.http) file for several examples of starting workflow by invoking the HTTP APIs. 48 | It works best if used with the [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) VS Code extension. 49 | 50 | For example, a purchase order workflow can be started with the following HTTP request: 51 | 52 | ```http 53 | POST http://localhost:8080/orders 54 | Content-Type: application/json 55 | 56 | { "name": "catfood", "quantity": 3, "totalCost": 19.99 } 57 | ``` 58 | 59 | The response will contain a `Location` header that looks something like `http://localhost:8080/orders/XYZ`, where `XYZ` is a randomly generated order ID. 60 | Follow this URL to get the workflow status as a JSON response. 61 | 62 | If the workflow requires approval, you can do so by sending a POST request to `http://localhost:8080/orders/XYZ/approve` where `XYZ` is the order ID. 63 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/Dapr.Workflow.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | Dapr.Workflow 12 | Dapr Workflow Client SDK 13 | Dapr Workflow client SDK for building workflows as code with Dapr 14 | true 15 | MIT 16 | $(RepositoryUrl) 17 | true 18 | true 19 | https://github.com/cgillum/durabletask-dapr 20 | 0.1.0 21 | alpha 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/InvokeServiceMethodActivity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json; 5 | using Dapr.Client; 6 | using Microsoft.DurableTask; 7 | using Newtonsoft.Json; 8 | 9 | namespace Dapr.Workflow; 10 | 11 | [DurableTask("DaprInvoke")] 12 | class InvokeServiceMethodActivity : TaskActivityBase 13 | { 14 | readonly DaprClient daprClient; 15 | 16 | public InvokeServiceMethodActivity(DaprClient daprClient) 17 | { 18 | this.daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); 19 | } 20 | 21 | protected override async Task OnRunAsync(TaskActivityContext context, InvokeArgs? input) 22 | { 23 | ArgumentNullException.ThrowIfNull(input, nameof(input)); 24 | 25 | HttpRequestMessage httpRequest = this.daprClient.CreateInvokeMethodRequest( 26 | input.HttpMethod, 27 | input.AppId, 28 | input.MethodName, 29 | input.Data); 30 | 31 | try 32 | { 33 | // TODO: Use HttpClient instead of DaprClient for service invocation. 34 | // See discussion in https://github.com/dapr/dotnet-sdk/issues/907 35 | JsonElement result = await this.daprClient.InvokeMethodAsync(httpRequest); 36 | return result; 37 | } 38 | catch (InvocationException e) when (e.InnerException is JsonReaderException) 39 | { 40 | // TODO: Need a more well-defined mechanism for handling deserialization issues. 41 | return new JsonElement(); 42 | } 43 | } 44 | } 45 | 46 | public record InvokeArgs(HttpMethod HttpMethod, string AppId, string MethodName, object Data); 47 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/WorkflowClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.DurableTask; 5 | using Microsoft.DurableTask.Grpc; 6 | 7 | namespace Dapr.Workflow; 8 | 9 | public sealed class WorkflowClient : IAsyncDisposable 10 | { 11 | // IMPORTANT: This client is only designed to work with the built-in engine. 12 | // The implementation will need to be updated to use the Dapr 13 | // workflow building block APIs instead so that it can be used 14 | // with alternate workflow components. 15 | readonly DurableTaskClient innerClient; 16 | 17 | internal WorkflowClient(IServiceProvider? services = null) 18 | { 19 | DurableTaskGrpcClient.Builder builder = new(); 20 | if (services != null) 21 | { 22 | builder.UseServices(services); 23 | } 24 | 25 | this.innerClient = builder.Build(); 26 | } 27 | 28 | public Task ScheduleNewWorkflowAsync( 29 | string name, 30 | string? instanceId = null, 31 | object? input = null, 32 | DateTime? startTime = null) 33 | { 34 | return this.innerClient.ScheduleNewOrchestrationInstanceAsync(name, instanceId, input, startTime); 35 | } 36 | 37 | public async Task GetWorkflowMetadata(string instanceId, bool getInputsAndOutputs = false) 38 | { 39 | OrchestrationMetadata? metadata = await this.innerClient.GetInstanceMetadataAsync( 40 | instanceId, 41 | getInputsAndOutputs); 42 | return new WorkflowMetadata(metadata); 43 | } 44 | 45 | public Task RaiseEventAsync(string instanceId, string eventName, object? eventData = null) 46 | { 47 | return this.innerClient.RaiseEventAsync(instanceId, eventName, eventData); 48 | } 49 | 50 | public Task TerminateWorkflowAsync(string instanceId, string reason) 51 | { 52 | return this.innerClient.TerminateAsync(instanceId, reason); 53 | } 54 | 55 | public ValueTask DisposeAsync() 56 | { 57 | return ((IAsyncDisposable)this.innerClient).DisposeAsync(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/WorkflowContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json; 5 | using Microsoft.DurableTask; 6 | 7 | namespace Dapr.Workflow; 8 | 9 | public class WorkflowContext 10 | { 11 | readonly TaskOrchestrationContext innerContext; 12 | 13 | internal WorkflowContext(TaskOrchestrationContext innerContext) 14 | { 15 | this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); 16 | } 17 | 18 | // TODO: Expose a "WorkflowId" type, similar to "ActorId" 19 | public string InstanceId => this.innerContext.InstanceId; 20 | 21 | public DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; 22 | 23 | public void SetCustomStatus(object? customStatus) => this.innerContext.SetCustomStatus(customStatus); 24 | 25 | // TODO: Should we keep the CreateTimer name to be consistent with the underlying SDK or nah? 26 | public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken = default) 27 | { 28 | return this.innerContext.CreateTimer(delay, cancellationToken); 29 | } 30 | 31 | public Task WaitForExternalEventAsync(string eventName, TimeSpan timeout) 32 | { 33 | return this.innerContext.WaitForExternalEvent(eventName, timeout); 34 | } 35 | 36 | public Task WaitForExternalEventAsync(string eventName, TimeSpan timeout) 37 | { 38 | return this.innerContext.WaitForExternalEvent(eventName, timeout); 39 | } 40 | 41 | public void PublishEvent(string pubSubName, string topic, object payload) 42 | { 43 | string pubSubId = $"dapr.pubsub://{pubSubName}"; 44 | this.innerContext.SendEvent(pubSubId, topic, payload); 45 | } 46 | 47 | public Task InvokeMethodAsync(HttpMethod httpMethod, string appId, string methodName, object data) 48 | { 49 | return this.innerContext.CallDaprInvokeAsync(new InvokeArgs(httpMethod, appId, methodName, data)); 50 | } 51 | 52 | public async Task InvokeMethodAsync( 53 | HttpMethod httpMethod, 54 | string appId, 55 | string methodName, 56 | object data) 57 | { 58 | InvokeArgs args = new(httpMethod, appId, methodName, data); 59 | JsonElement json = await this.innerContext.CallDaprInvokeAsync(args); 60 | return JsonSerializer.Deserialize(json); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Dapr.Workflow/WorkflowMetadata.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.DurableTask; 5 | 6 | namespace Dapr.Workflow; 7 | 8 | public class WorkflowMetadata 9 | { 10 | internal WorkflowMetadata(OrchestrationMetadata? metadata) 11 | { 12 | this.Details = metadata; 13 | } 14 | 15 | public bool Exists => this.Details != null; 16 | 17 | public bool IsWorkflowRunning => this.Details?.RuntimeStatus == OrchestrationRuntimeStatus.Running; 18 | 19 | public OrchestrationMetadata? Details { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/WorkflowRuntimeOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.DurableTask; 5 | 6 | namespace Dapr.Workflow; 7 | 8 | public sealed class WorkflowRuntimeOptions 9 | { 10 | readonly Dictionary> factories = new(); 11 | 12 | public void RegisterWorkflow(string name, Func> implementation) 13 | { 14 | // Dapr workflows are implemented as specialized Durable Task orchestrations 15 | this.factories.Add(name, (IDurableTaskRegistry registry) => 16 | { 17 | registry.AddOrchestrator(name, (innerContext, input) => 18 | { 19 | WorkflowContext workflowContext = new(innerContext); 20 | return implementation(workflowContext, input); 21 | }); 22 | }); 23 | } 24 | 25 | // TODO: Add support for activities 26 | 27 | internal void AddWorkflowsToRegistry(IDurableTaskRegistry registry) 28 | { 29 | foreach (Action factory in this.factories.Values) 30 | { 31 | factory.Invoke(registry); 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.DurableTask.Grpc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Dapr.Workflow; 8 | 9 | // TODO: This belongs in an ASP.NET Core project/namespace 10 | 11 | /// 12 | /// Contains extension methods for using Dapr Actors with dependency injection. 13 | /// 14 | public static class WorkflowServiceCollectionExtensions 15 | { 16 | /// 17 | /// Adds Dapr Workflow support to the service collection. 18 | /// 19 | /// The . 20 | /// A delegate used to configure actor options and register workflow functions. 21 | public static IServiceCollection AddWorkflow( 22 | this IServiceCollection serviceCollection, 23 | Action configure) 24 | { 25 | if (serviceCollection == null) 26 | { 27 | throw new ArgumentNullException(nameof(serviceCollection)); 28 | } 29 | 30 | serviceCollection.AddSingleton(); 31 | serviceCollection.AddDaprClient(); 32 | serviceCollection.AddSingleton(services => new WorkflowClient(services)); 33 | 34 | serviceCollection.AddHostedService(services => 35 | { 36 | DurableTaskGrpcWorker.Builder workerBuilder = DurableTaskGrpcWorker.CreateBuilder().UseServices(services); 37 | 38 | WorkflowRuntimeOptions options = services.GetRequiredService(); 39 | configure?.Invoke(options); 40 | 41 | workerBuilder.UseServices(services); 42 | 43 | workerBuilder.AddTasks(registry => 44 | { 45 | options.AddWorkflowsToRegistry(registry); 46 | 47 | // Built-in method for doing service invocation 48 | registry.AddActivity(); 49 | 50 | // TODO: Built-in activity for invoking output bindings 51 | }); 52 | 53 | DurableTaskGrpcWorker worker = workerBuilder.Build(); 54 | return worker; 55 | }); 56 | 57 | return serviceCollection; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/DaprOrchestrationService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Diagnostics; 5 | using System.Threading.Channels; 6 | using Dapr.Actors; 7 | using Dapr.Actors.Client; 8 | using DurableTask.Core; 9 | using DurableTask.Core.History; 10 | using DurableTask.Dapr.Activities; 11 | using DurableTask.Dapr.Workflows; 12 | using Microsoft.AspNetCore.Builder; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Hosting; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace DurableTask.Dapr; 18 | 19 | /// 20 | /// The primary integration point for the Durable Task Framework and Dapr actors. 21 | /// 22 | /// 23 | /// 24 | /// Durable Task Framework apps can use Dapr actors as the underlying storage provider and scheduler by creating 25 | /// instances of this class and passing them in as the constructor arguments for the and 26 | /// objects. The client and worker will then call into class via the 27 | /// and interfaces, respectively. 28 | /// 29 | /// In this orchestration service, each created orchestration instance maps a Dapr actor instance (it may map to more 30 | /// than one actor in a future iteration). The actor stores the orchestration history and metadata in its own internal 31 | /// state. Operations invoked on the actor will either query the orchestration state or trigger the orchestration to 32 | /// be executed in the current process. 33 | /// 34 | /// 35 | public class DaprOrchestrationService : OrchestrationServiceBase, IWorkflowExecutor, IActivityExecutor 36 | { 37 | /// 38 | /// Dapr-specific configuration options for this orchestration service. 39 | /// 40 | readonly DaprOptions options; 41 | 42 | /// 43 | /// Channel used to asynchronously invoke the orchestration when certain actor messages are received. 44 | /// 45 | readonly Channel orchestrationWorkItemChannel; 46 | 47 | /// 48 | /// Channel used to asynchronously invoke an activity when certain actor messages are received. 49 | /// 50 | readonly Channel activityWorkItemChannel; 51 | 52 | /// 53 | /// The web host that routes HTTP requests to specific actor instances. 54 | /// 55 | readonly IHost daprActorHost; 56 | 57 | /// 58 | /// Initializes a new instance of the class with the specified configuration 59 | /// options. 60 | /// 61 | /// Configuration options for Dapr integration. 62 | public DaprOrchestrationService(DaprOptions options) 63 | { 64 | this.options = options ?? throw new ArgumentNullException(nameof(options)); 65 | 66 | this.orchestrationWorkItemChannel = Channel.CreateUnbounded(); 67 | this.activityWorkItemChannel = Channel.CreateUnbounded(); 68 | 69 | // The actor host is an HTTP service that routes incoming requests to actor instances. 70 | WebApplicationBuilder builder = WebApplication.CreateBuilder(); 71 | if (options.LoggerFactory != null) 72 | { 73 | builder.Services.AddSingleton(options.LoggerFactory); 74 | } 75 | 76 | builder.Services.AddActors(options => 77 | { 78 | options.Actors.RegisterActor(); 79 | options.Actors.RegisterActor(); 80 | }); 81 | 82 | // Register the orchestration service as a dependency so that the actors can invoke methods on it. 83 | builder.Services.AddSingleton(this); 84 | builder.Services.AddSingleton(this); 85 | 86 | WebApplication app = builder.Build(); 87 | app.UseRouting(); 88 | app.UseEndpoints(endpoints => endpoints.MapActorsHandlers()); 89 | this.daprActorHost = app; 90 | } 91 | 92 | #region Task Hub Management 93 | // Nothing to do, since we rely on the existing Dapr actor infrastructure to already be there. 94 | public override Task CreateAsync(bool recreateInstanceStore) => Task.CompletedTask; 95 | 96 | // Nothing to do, since we rely on the existing Dapr actor infrastructure to already be there. 97 | public override Task CreateIfNotExistsAsync() => Task.CompletedTask; 98 | 99 | // REVIEW: Would it make sense to so something here, like delete any created resources? 100 | public override Task DeleteAsync(bool deleteInstanceStore) => Task.CompletedTask; 101 | #endregion 102 | 103 | #region Client APIs (called by TaskHubClient) 104 | public override async Task CreateTaskOrchestrationAsync( 105 | TaskMessage creationMessage, 106 | OrchestrationStatus[] dedupeStatuses) 107 | { 108 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(creationMessage.OrchestrationInstance.InstanceId); 109 | 110 | // Depending on where the actor gets placed, this may invoke an actor on another machine. 111 | await proxy.InitAsync(creationMessage, dedupeStatuses); 112 | } 113 | 114 | public override async Task SendTaskOrchestrationMessageAsync(TaskMessage message) 115 | { 116 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(message.OrchestrationInstance.InstanceId); 117 | await proxy.PostToInboxAsync(message); 118 | } 119 | 120 | public override async Task WaitForOrchestrationAsync( 121 | string instanceId, 122 | string executionId, 123 | TimeSpan timeout, 124 | CancellationToken cancellationToken) 125 | { 126 | Stopwatch sw = Stopwatch.StartNew(); 127 | 128 | do 129 | { 130 | OrchestrationState? state = await this.GetOrchestrationStateAsync(instanceId, executionId); 131 | if (state != null && ( 132 | state.OrchestrationStatus == OrchestrationStatus.Completed || 133 | state.OrchestrationStatus == OrchestrationStatus.Failed || 134 | state.OrchestrationStatus == OrchestrationStatus.Terminated)) 135 | { 136 | return state; 137 | } 138 | 139 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); 140 | } 141 | while (timeout == Timeout.InfiniteTimeSpan || sw.Elapsed < timeout); 142 | 143 | throw new TimeoutException(); 144 | } 145 | 146 | public override async Task GetOrchestrationStateAsync(string instanceId, string? executionId) 147 | { 148 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(instanceId); 149 | OrchestrationState? state = await proxy.GetCurrentStateAsync(); 150 | return state; 151 | } 152 | 153 | public override Task PurgeOrchestrationHistoryAsync( 154 | DateTime thresholdDateTimeUtc, 155 | OrchestrationStateTimeRangeFilterType timeRangeFilterType) 156 | { 157 | throw new NotImplementedException(); 158 | } 159 | #endregion 160 | 161 | #region Worker APIs 162 | 163 | public override Task AbandonTaskActivityWorkItemAsync(TaskActivityWorkItem workItem) 164 | { 165 | throw new NotImplementedException(); 166 | } 167 | 168 | public override Task AbandonTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) 169 | { 170 | WorkflowExecutionWorkItem workflowWorkItem = (WorkflowExecutionWorkItem)workItem; 171 | 172 | // Call back into the actor via a TaskCompletionSource to reschedule the work-item 173 | workflowWorkItem.TaskCompletionSource.SetResult(new WorkflowExecutionResult( 174 | ExecutionResultType.Abandoned, 175 | null!, 176 | Array.Empty(), 177 | Array.Empty(), 178 | Array.Empty(), 179 | Array.Empty())); 180 | 181 | return Task.CompletedTask; 182 | } 183 | 184 | public override Task CompleteTaskActivityWorkItemAsync(TaskActivityWorkItem workItem, TaskMessage responseMessage) 185 | { 186 | ActivityExecutionWorkItem activityExecutionWorkItem = (ActivityExecutionWorkItem)workItem; 187 | TaskScheduledEvent scheduledEvent = (TaskScheduledEvent)workItem.TaskMessage.Event; 188 | 189 | ActivityCompletionResponse response = new() { TaskId = scheduledEvent.EventId }; 190 | if (responseMessage.Event is TaskCompletedEvent completedEvent) 191 | { 192 | response.SerializedResult = completedEvent.Result; 193 | } 194 | else 195 | { 196 | TaskFailedEvent failedEvent = (TaskFailedEvent)responseMessage.Event; 197 | response.FailureDetails = failedEvent.FailureDetails; 198 | } 199 | 200 | activityExecutionWorkItem.TaskCompletionSource.SetResult(response); 201 | return Task.CompletedTask; 202 | } 203 | 204 | public override Task CompleteTaskOrchestrationWorkItemAsync( 205 | TaskOrchestrationWorkItem workItem, 206 | OrchestrationRuntimeState newOrchestrationRuntimeState, 207 | IList outboundMessages, 208 | IList orchestratorMessages, 209 | IList timerMessages, 210 | TaskMessage continuedAsNewMessage, 211 | OrchestrationState orchestrationState) 212 | { 213 | WorkflowExecutionWorkItem workflowWorkItem = (WorkflowExecutionWorkItem)workItem; 214 | 215 | // Call back into the actor via a TaskCompletionSource to save the state 216 | // TODO: continuedAsNewMessage (does this still apply?) 217 | workflowWorkItem.TaskCompletionSource.SetResult(new WorkflowExecutionResult( 218 | Type: ExecutionResultType.Executed, 219 | UpdatedState: orchestrationState, 220 | Timers: timerMessages, 221 | ActivityOutbox: outboundMessages, 222 | OrchestrationOutbox: orchestratorMessages, 223 | NewHistoryEvents: newOrchestrationRuntimeState.NewEvents)); 224 | 225 | return Task.CompletedTask; 226 | } 227 | 228 | public override async Task LockNextTaskActivityWorkItem( 229 | TimeSpan receiveTimeout, 230 | CancellationToken cancellationToken) 231 | { 232 | return await this.activityWorkItemChannel.Reader.ReadAsync(cancellationToken); 233 | } 234 | 235 | public override async Task LockNextTaskOrchestrationWorkItemAsync( 236 | TimeSpan receiveTimeout, 237 | CancellationToken cancellationToken) 238 | { 239 | return await this.orchestrationWorkItemChannel.Reader.ReadAsync(cancellationToken); 240 | } 241 | 242 | public override Task RenewTaskActivityWorkItemLockAsync(TaskActivityWorkItem workItem) 243 | { 244 | // Not used 245 | return Task.FromResult(new TaskActivityWorkItem()); 246 | } 247 | 248 | public override Task RenewTaskOrchestrationWorkItemLockAsync(TaskOrchestrationWorkItem workItem) 249 | { 250 | // Not used 251 | return Task.CompletedTask; 252 | } 253 | 254 | // Called by the TaskHubWorker 255 | public override Task StartAsync() => this.daprActorHost.StartAsync(); 256 | 257 | // Called by the TaskHubWorker 258 | public override Task StopAsync() => this.daprActorHost.StopAsync(); 259 | 260 | #endregion 261 | 262 | #region Actor API calls 263 | 264 | // NOTE: This is just glue code to make the IOrchestrationService's polling abstraction invocable as a method 265 | Task IWorkflowExecutor.ExecuteWorkflowStepAsync( 266 | string instanceId, 267 | IList inbox, 268 | IList history) 269 | { 270 | WorkflowExecutionWorkItem workItem = new() 271 | { 272 | InstanceId = instanceId, 273 | NewMessages = inbox, 274 | OrchestrationRuntimeState = new OrchestrationRuntimeState(history), 275 | }; 276 | 277 | // The IOrchestrationService.LockNextTaskOrchestrationWorkItemAsync method 278 | // is listening for new work-items on this channel. 279 | if (!this.orchestrationWorkItemChannel.Writer.TryWrite(workItem)) 280 | { 281 | return Task.FromResult(new WorkflowExecutionResult( 282 | ExecutionResultType.Throttled, 283 | null!, 284 | Array.Empty(), 285 | Array.Empty(), 286 | Array.Empty(), 287 | Array.Empty())); 288 | } 289 | 290 | // The IOrchestrationService.CompleteTaskOrchestrationWorkItemAsync method 291 | // is expected to set the result for this task. 292 | return workItem.TaskCompletionSource.Task; 293 | } 294 | 295 | // NOTE: This is just glue code to make the IOrchestrationService's polling abstraction invocable as a method 296 | Task IActivityExecutor.ExecuteActivityAsync(ActivityInvocationRequest request) 297 | { 298 | ActivityExecutionWorkItem workItem = new() 299 | { 300 | Id = $"{request.InstanceId}:{request.TaskId:X16}", 301 | LockedUntilUtc = DateTime.MaxValue, 302 | TaskMessage = new TaskMessage() 303 | { 304 | OrchestrationInstance = new OrchestrationInstance 305 | { 306 | InstanceId = request.InstanceId, 307 | ExecutionId = request.ExecutionId, 308 | }, 309 | Event = new TaskScheduledEvent(request.TaskId) 310 | { 311 | Name = request.ActivityName, 312 | Input = request.SerializedInput, 313 | }, 314 | }, 315 | }; 316 | 317 | // IOrchestrationService.LockNextTaskActivityWorkItemAsync is listening for new work-items on this channel. 318 | if (!this.activityWorkItemChannel.Writer.TryWrite(workItem)) 319 | { 320 | // TODO 321 | } 322 | 323 | // The IOrchestrationService.CompleteTaskActivityWorkItemAsync method 324 | // is expected to set the result for this task. 325 | return workItem.TaskCompletionSource.Task; 326 | } 327 | 328 | #endregion 329 | 330 | IWorkflowActor GetOrchestrationActorProxy(string instanceId, TimeSpan? timeout = null) 331 | { 332 | // REVIEW: Should we be caching these proxy objects? 333 | return ActorProxy.Create( 334 | new ActorId(instanceId), 335 | nameof(WorkflowActor), 336 | new ActorProxyOptions { RequestTimeout = timeout }); 337 | } 338 | 339 | class WorkflowExecutionWorkItem : TaskOrchestrationWorkItem 340 | { 341 | public TaskCompletionSource TaskCompletionSource { get; } = new(); 342 | } 343 | 344 | class ActivityExecutionWorkItem : TaskActivityWorkItem 345 | { 346 | public TaskCompletionSource TaskCompletionSource { get; } = new(); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Activities/ActivityCompletionResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.Serialization; 5 | using DurableTask.Core; 6 | 7 | namespace DurableTask.Dapr.Activities; 8 | 9 | /// 10 | /// The result of a workflow activity execution. 11 | /// 12 | [DataContract] 13 | public class ActivityCompletionResponse 14 | { 15 | /// 16 | /// Gets the orchestration-specific ID of the activity task. 17 | /// 18 | [DataMember] 19 | public int TaskId { get; init; } 20 | 21 | /// 22 | /// Gets the serialized output of the activity. 23 | /// 24 | [DataMember] 25 | public string? SerializedResult { get; set; } 26 | 27 | /// 28 | /// If the activity execution resulted in a failure, gets the details of the failure. 29 | /// Otherwise, this property will return null. 30 | /// 31 | [DataMember] 32 | public FailureDetails? FailureDetails { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Activities/ActivityInvocationRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.Serialization; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace DurableTask.Dapr.Activities; 8 | 9 | /// 10 | /// The parameters of an activity invocation. 11 | /// 12 | [DataContract] 13 | public class ActivityInvocationRequest 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | [JsonConstructor] 19 | public ActivityInvocationRequest( 20 | string activityName, 21 | int taskId, 22 | string? serializedInput, 23 | string instanceId, 24 | string executionId) 25 | { 26 | this.ActivityName = activityName; 27 | this.TaskId = taskId; 28 | this.SerializedInput = serializedInput; 29 | this.InstanceId = instanceId; 30 | this.ExecutionId = executionId; 31 | } 32 | 33 | /// 34 | /// The name of the activity to invoke. 35 | /// 36 | [DataMember] 37 | public string ActivityName { get; private set; } 38 | 39 | /// 40 | /// The ID of the activity task. 41 | /// 42 | [DataMember] 43 | public int TaskId { get; private set; } 44 | 45 | /// 46 | /// The serialized input of the activity. 47 | /// 48 | [DataMember] 49 | public string? SerializedInput { get; private set; } 50 | 51 | /// 52 | /// Gets the orchestration instance ID associated with this activity. 53 | /// 54 | [DataMember] 55 | public string? InstanceId { get; private set; } 56 | 57 | /// 58 | /// Gets the orchestration instance execution ID associated with this activity. 59 | /// 60 | [DataMember] 61 | public string? ExecutionId { get; private set; } 62 | 63 | // An activity can be identified by it's name followed by it's task ID. Example: SayHello#0, SayHello#1, etc. 64 | internal string Identifier => $"{this.ActivityName}#{this.TaskId}"; 65 | 66 | /// 67 | /// Returns an activity identifier string in the form of {activityName}#{taskID}. 68 | /// 69 | public override string ToString() => this.Identifier; 70 | } -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Activities/IActivityActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Dapr.Actors; 5 | 6 | namespace DurableTask.Dapr.Activities; 7 | 8 | /// 9 | /// Interface for the workflow activity actor. 10 | /// 11 | public interface IActivityActor : IActor 12 | { 13 | /// 14 | /// Triggers an activity to execute. 15 | /// 16 | /// 17 | /// The result of the activity is delivered asynchronously back to the orchestration that scheduled it. 18 | /// 19 | /// The activity invocation parameters. 20 | /// Returns a task that completes when the activity invocation is reliably scheduled. 21 | Task InvokeAsync(ActivityInvocationRequest request); 22 | } 23 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Activities/IActivityExecutor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace DurableTask.Dapr.Activities; 5 | 6 | /// 7 | /// Interface for objects that can implement activity execution logic. 8 | /// 9 | interface IActivityExecutor 10 | { 11 | /// 12 | /// Executes the activities logic and returns the results. 13 | /// 14 | /// The activity execution parameters. 15 | /// The result of the activity execution. 16 | Task ExecuteActivityAsync(ActivityInvocationRequest request); 17 | } 18 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Activities/StatelessActivityActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json; 5 | using Dapr.Actors; 6 | using Dapr.Actors.Client; 7 | using Dapr.Actors.Runtime; 8 | using Dapr.Client; 9 | using DurableTask.Core; 10 | using DurableTask.Dapr.Workflows; 11 | 12 | namespace DurableTask.Dapr.Activities; 13 | 14 | /// 15 | /// A stateless, reliable actor that executes activity logic. 16 | /// 17 | class StatelessActivityActor : ReliableActor, IActivityActor 18 | { 19 | readonly DaprClient daprClient; 20 | readonly IActivityExecutor activityInvoker; 21 | 22 | public StatelessActivityActor( 23 | ActorHost host, 24 | DaprOptions options, 25 | DaprClient daprClient, 26 | IActivityExecutor activityInvoker) 27 | : base(host, options) 28 | { 29 | this.daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); 30 | this.activityInvoker = activityInvoker ?? throw new ArgumentNullException(nameof(activityInvoker)); 31 | } 32 | 33 | Task IActivityActor.InvokeAsync(ActivityInvocationRequest request) 34 | { 35 | // Persist the request in a reminder to 1) unblock the calling orchestrator and 2) ensure reliable execution. 36 | byte[] reminderState = JsonSerializer.SerializeToUtf8Bytes(request); 37 | return this.CreateReliableReminder(reminderName: "execute", reminderState); 38 | } 39 | 40 | protected override async Task OnReminderReceivedAsync(string reminderName, byte[] state) 41 | { 42 | ActivityInvocationRequest? request = null; 43 | try 44 | { 45 | request = JsonSerializer.Deserialize(state); 46 | } 47 | catch (Exception e) 48 | { 49 | this.Log.ActivityActorWarning( 50 | this.Id, 51 | $"Failed to deserialize the activity invocation request from the reminder state: {e}"); 52 | await this.UnregisterReminderAsync(reminderName); 53 | return; 54 | } 55 | 56 | if (request == null) 57 | { 58 | this.Log.ActivityActorWarning( 59 | this.Id, 60 | $"Failed to deserialize the activity invocation request from the reminder state."); 61 | await this.UnregisterReminderAsync(reminderName); 62 | return; 63 | } 64 | 65 | if (request.InstanceId == null) 66 | { 67 | this.Log.ActivityActorWarning(this.Id, $"Couldn't find the orchestration instance ID state."); 68 | await this.UnregisterReminderAsync(reminderName); 69 | return; 70 | } 71 | 72 | ActivityCompletionResponse response; 73 | try 74 | { 75 | response = await this.activityInvoker.ExecuteActivityAsync(request); 76 | } 77 | catch (Exception e) 78 | { 79 | response = new ActivityCompletionResponse() 80 | { 81 | TaskId = request.TaskId, 82 | FailureDetails = new FailureDetails(e), 83 | }; 84 | } 85 | 86 | // Asynchronously call back into the workflow actor with the result of the activity execution. 87 | // REVIEW: Should this proxy be cached? 88 | IWorkflowActor proxy = ActorProxy.Create( 89 | new ActorId(request.InstanceId), 90 | nameof(WorkflowActor), 91 | new ActorProxyOptions()); 92 | await proxy.CompleteActivityAsync(response); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/DaprOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | 7 | namespace DurableTask.Dapr; 8 | 9 | public class DaprOptions 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public DaprOptions(ILoggerFactory? loggerFactory = null) 15 | { 16 | this.LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; 17 | } 18 | 19 | /// 20 | /// The to use for both the Dapr orchestration service and the ASP.NET Core host. 21 | /// 22 | public ILoggerFactory LoggerFactory { get; init; } 23 | 24 | /// 25 | /// The interval time for reminders that are created specifically for reliability. The default value is 5 minutes. 26 | /// 27 | /// 28 | /// A value of or less will disable reminder intervals. 29 | /// 30 | public TimeSpan ReliableReminderInterval { get; set; } = TimeSpan.FromMinutes(5); 31 | 32 | /// 33 | /// The gRPC endpoint to use when communicating with the Dapr sidecar over gRPC. 34 | /// 35 | /// 36 | /// This value is typically in the form "http://127.0.0.1:%DAPR_GRPC_PORT%". 37 | /// 38 | public string? GrpcEndpoint { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/DaprOrchestrationService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Diagnostics; 5 | using System.Threading.Channels; 6 | using Dapr.Actors; 7 | using Dapr.Actors.Client; 8 | using DurableTask.Core; 9 | using DurableTask.Core.History; 10 | using DurableTask.Dapr.Activities; 11 | using DurableTask.Dapr.Workflows; 12 | using Microsoft.AspNetCore.Builder; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Hosting; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace DurableTask.Dapr; 18 | 19 | /// 20 | /// The primary integration point for the Durable Task Framework and Dapr actors. 21 | /// 22 | /// 23 | /// 24 | /// Durable Task Framework apps can use Dapr actors as the underlying storage provider and scheduler by creating 25 | /// instances of this class and passing them in as the constructor arguments for the and 26 | /// objects. The client and worker will then call into class via the 27 | /// and interfaces, respectively. 28 | /// 29 | /// In this orchestration service, each created orchestration instance maps a Dapr actor instance (it may map to more 30 | /// than one actor in a future iteration). The actor stores the orchestration history and metadata in its own internal 31 | /// state. Operations invoked on the actor will either query the orchestration state or trigger the orchestration to 32 | /// be executed in the current process. 33 | /// 34 | /// 35 | public class DaprOrchestrationService : OrchestrationServiceBase, IWorkflowExecutor, IActivityExecutor 36 | { 37 | /// 38 | /// Dapr-specific configuration options for this orchestration service. 39 | /// 40 | readonly DaprOptions options; 41 | 42 | /// 43 | /// Channel used to asynchronously invoke the orchestration when certain actor messages are received. 44 | /// 45 | readonly Channel orchestrationWorkItemChannel; 46 | 47 | /// 48 | /// Channel used to asynchronously invoke an activity when certain actor messages are received. 49 | /// 50 | readonly Channel activityWorkItemChannel; 51 | 52 | /// 53 | /// The web host that routes HTTP requests to specific actor instances. 54 | /// 55 | readonly IHost daprActorHost; 56 | 57 | /// 58 | /// Initializes a new instance of the class with the specified configuration 59 | /// options. 60 | /// 61 | /// Configuration options for Dapr integration. 62 | public DaprOrchestrationService(DaprOptions options) 63 | { 64 | this.options = options ?? throw new ArgumentNullException(nameof(options)); 65 | 66 | this.orchestrationWorkItemChannel = Channel.CreateUnbounded(); 67 | this.activityWorkItemChannel = Channel.CreateUnbounded(); 68 | 69 | // The actor host is an HTTP service that routes incoming requests to actor instances. 70 | WebApplicationBuilder builder = WebApplication.CreateBuilder(); 71 | if (options.LoggerFactory != null) 72 | { 73 | builder.Services.AddSingleton(options.LoggerFactory); 74 | } 75 | 76 | builder.Services.AddDaprClient(clientOptions => 77 | { 78 | if (!string.IsNullOrEmpty(options.GrpcEndpoint)) 79 | { 80 | clientOptions.UseGrpcEndpoint(options.GrpcEndpoint); 81 | } 82 | }); 83 | 84 | builder.Services.AddActors(actorOptions => 85 | { 86 | actorOptions.Actors.RegisterActor(); 87 | actorOptions.Actors.RegisterActor(); 88 | 89 | // The Durable Task Framework history events are not compatible with System.Text.Json so we need to create 90 | // a custom converter for these. 91 | actorOptions.JsonSerializerOptions.Converters.Add(new DurableTaskHistoryConverter()); 92 | actorOptions.JsonSerializerOptions.Converters.Add(new DurableTaskTimerFiredConverter()); 93 | }); 94 | 95 | // Register the orchestration service as a dependency so that the actors can invoke methods on it. 96 | builder.Services.AddSingleton(this); 97 | builder.Services.AddSingleton(this); 98 | builder.Services.AddSingleton(options); 99 | 100 | WebApplication app = builder.Build(); 101 | app.UseRouting(); 102 | app.UseEndpoints(endpoints => endpoints.MapActorsHandlers()); 103 | this.daprActorHost = app; 104 | } 105 | 106 | #region Task Hub Management 107 | // Nothing to do, since we rely on the existing Dapr actor infrastructure to already be there. 108 | public override Task CreateAsync(bool recreateInstanceStore) => Task.CompletedTask; 109 | 110 | // Nothing to do, since we rely on the existing Dapr actor infrastructure to already be there. 111 | public override Task CreateIfNotExistsAsync() => Task.CompletedTask; 112 | 113 | // REVIEW: Would it make sense to so something here, like delete any created resources? 114 | public override Task DeleteAsync(bool deleteInstanceStore) => Task.CompletedTask; 115 | #endregion 116 | 117 | #region Client APIs (called by TaskHubClient) 118 | public override async Task CreateTaskOrchestrationAsync( 119 | TaskMessage creationMessage, 120 | OrchestrationStatus[] dedupeStatuses) 121 | { 122 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(creationMessage.OrchestrationInstance.InstanceId); 123 | 124 | // Depending on where the actor gets placed, this may invoke an actor on another machine. 125 | await proxy.InitAsync(creationMessage, dedupeStatuses); 126 | } 127 | 128 | public override async Task SendTaskOrchestrationMessageAsync(TaskMessage message) 129 | { 130 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(message.OrchestrationInstance.InstanceId); 131 | await proxy.PostToInboxAsync(message); 132 | } 133 | 134 | public override async Task WaitForOrchestrationAsync( 135 | string instanceId, 136 | string executionId, 137 | TimeSpan timeout, 138 | CancellationToken cancellationToken) 139 | { 140 | Stopwatch sw = Stopwatch.StartNew(); 141 | 142 | do 143 | { 144 | OrchestrationState? state = await this.GetOrchestrationStateAsync(instanceId, executionId); 145 | if (state != null && ( 146 | state.OrchestrationStatus == OrchestrationStatus.Completed || 147 | state.OrchestrationStatus == OrchestrationStatus.Failed || 148 | state.OrchestrationStatus == OrchestrationStatus.Terminated)) 149 | { 150 | return state; 151 | } 152 | 153 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); 154 | } 155 | while (timeout == Timeout.InfiniteTimeSpan || sw.Elapsed < timeout); 156 | 157 | throw new TimeoutException(); 158 | } 159 | 160 | public override async Task GetOrchestrationStateAsync(string instanceId, string? executionId) 161 | { 162 | IWorkflowActor proxy = this.GetOrchestrationActorProxy(instanceId); 163 | OrchestrationState? state = await proxy.GetCurrentStateAsync(); 164 | return state; 165 | } 166 | 167 | public override Task PurgeOrchestrationHistoryAsync( 168 | DateTime thresholdDateTimeUtc, 169 | OrchestrationStateTimeRangeFilterType timeRangeFilterType) 170 | { 171 | throw new NotImplementedException(); 172 | } 173 | #endregion 174 | 175 | #region Worker APIs 176 | 177 | public override Task AbandonTaskActivityWorkItemAsync(TaskActivityWorkItem workItem) 178 | { 179 | throw new NotImplementedException(); 180 | } 181 | 182 | public override Task AbandonTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) 183 | { 184 | WorkflowExecutionWorkItem workflowWorkItem = (WorkflowExecutionWorkItem)workItem; 185 | 186 | // Call back into the actor via a TaskCompletionSource to reschedule the work-item 187 | workflowWorkItem.TaskCompletionSource.SetResult(new WorkflowExecutionResult( 188 | ExecutionResultType.Abandoned, 189 | null!, 190 | Array.Empty(), 191 | Array.Empty(), 192 | Array.Empty(), 193 | Array.Empty())); 194 | 195 | return Task.CompletedTask; 196 | } 197 | 198 | public override Task CompleteTaskActivityWorkItemAsync(TaskActivityWorkItem workItem, TaskMessage responseMessage) 199 | { 200 | ActivityExecutionWorkItem activityExecutionWorkItem = (ActivityExecutionWorkItem)workItem; 201 | TaskScheduledEvent scheduledEvent = (TaskScheduledEvent)workItem.TaskMessage.Event; 202 | 203 | ActivityCompletionResponse response = new() { TaskId = scheduledEvent.EventId }; 204 | if (responseMessage.Event is TaskCompletedEvent completedEvent) 205 | { 206 | response.SerializedResult = completedEvent.Result; 207 | } 208 | else 209 | { 210 | TaskFailedEvent failedEvent = (TaskFailedEvent)responseMessage.Event; 211 | response.FailureDetails = failedEvent.FailureDetails; 212 | } 213 | 214 | activityExecutionWorkItem.TaskCompletionSource.SetResult(response); 215 | return Task.CompletedTask; 216 | } 217 | 218 | public override Task CompleteTaskOrchestrationWorkItemAsync( 219 | TaskOrchestrationWorkItem workItem, 220 | OrchestrationRuntimeState newOrchestrationRuntimeState, 221 | IList outboundMessages, 222 | IList orchestratorMessages, 223 | IList timerMessages, 224 | TaskMessage continuedAsNewMessage, 225 | OrchestrationState orchestrationState) 226 | { 227 | WorkflowExecutionWorkItem workflowWorkItem = (WorkflowExecutionWorkItem)workItem; 228 | 229 | // Call back into the actor via a TaskCompletionSource to save the state 230 | // TODO: continuedAsNewMessage (does this still apply?) 231 | workflowWorkItem.TaskCompletionSource.SetResult(new WorkflowExecutionResult( 232 | Type: ExecutionResultType.Executed, 233 | UpdatedState: orchestrationState, 234 | Timers: timerMessages, 235 | ActivityOutbox: outboundMessages, 236 | OrchestrationOutbox: orchestratorMessages, 237 | NewHistoryEvents: newOrchestrationRuntimeState.NewEvents)); 238 | 239 | return Task.CompletedTask; 240 | } 241 | 242 | public override async Task LockNextTaskActivityWorkItem( 243 | TimeSpan receiveTimeout, 244 | CancellationToken cancellationToken) 245 | { 246 | return await this.activityWorkItemChannel.Reader.ReadAsync(cancellationToken); 247 | } 248 | 249 | public override async Task LockNextTaskOrchestrationWorkItemAsync( 250 | TimeSpan receiveTimeout, 251 | CancellationToken cancellationToken) 252 | { 253 | return await this.orchestrationWorkItemChannel.Reader.ReadAsync(cancellationToken); 254 | } 255 | 256 | public override Task RenewTaskActivityWorkItemLockAsync(TaskActivityWorkItem workItem) 257 | { 258 | // Not used 259 | return Task.FromResult(new TaskActivityWorkItem()); 260 | } 261 | 262 | public override Task RenewTaskOrchestrationWorkItemLockAsync(TaskOrchestrationWorkItem workItem) 263 | { 264 | // Not used 265 | return Task.CompletedTask; 266 | } 267 | 268 | // Called by the TaskHubWorker 269 | public override async Task StartAsync() 270 | { 271 | await this.daprActorHost.StartAsync(); 272 | 273 | // Dapr requires several seconds for actor discovery to happen 274 | await Task.Delay(TimeSpan.FromSeconds(5)); 275 | } 276 | 277 | // Called by the TaskHubWorker 278 | public override Task StopAsync() => this.daprActorHost.StopAsync(); 279 | 280 | #endregion 281 | 282 | #region Actor API calls 283 | 284 | // NOTE: This is just glue code to make the IOrchestrationService's polling abstraction invocable as a method 285 | Task IWorkflowExecutor.ExecuteWorkflowStepAsync( 286 | string instanceId, 287 | IList inbox, 288 | IList history) 289 | { 290 | WorkflowExecutionWorkItem workItem = new() 291 | { 292 | InstanceId = instanceId, 293 | NewMessages = inbox, 294 | OrchestrationRuntimeState = new OrchestrationRuntimeState(history), 295 | }; 296 | 297 | // The IOrchestrationService.LockNextTaskOrchestrationWorkItemAsync method 298 | // is listening for new work-items on this channel. 299 | if (!this.orchestrationWorkItemChannel.Writer.TryWrite(workItem)) 300 | { 301 | return Task.FromResult(new WorkflowExecutionResult( 302 | ExecutionResultType.Throttled, 303 | null!, 304 | Array.Empty(), 305 | Array.Empty(), 306 | Array.Empty(), 307 | Array.Empty())); 308 | } 309 | 310 | // The IOrchestrationService.CompleteTaskOrchestrationWorkItemAsync method 311 | // is expected to set the result for this task. 312 | return workItem.TaskCompletionSource.Task; 313 | } 314 | 315 | // NOTE: This is just glue code to make the IOrchestrationService's polling abstraction invocable as a method 316 | Task IActivityExecutor.ExecuteActivityAsync(ActivityInvocationRequest request) 317 | { 318 | ActivityExecutionWorkItem workItem = new() 319 | { 320 | Id = $"{request.InstanceId}:{request.TaskId:X16}", 321 | LockedUntilUtc = DateTime.MaxValue, 322 | TaskMessage = new TaskMessage() 323 | { 324 | OrchestrationInstance = new OrchestrationInstance 325 | { 326 | InstanceId = request.InstanceId, 327 | ExecutionId = request.ExecutionId, 328 | }, 329 | Event = new TaskScheduledEvent(request.TaskId) 330 | { 331 | Name = request.ActivityName, 332 | Input = request.SerializedInput, 333 | }, 334 | }, 335 | }; 336 | 337 | // IOrchestrationService.LockNextTaskActivityWorkItemAsync is listening for new work-items on this channel. 338 | if (!this.activityWorkItemChannel.Writer.TryWrite(workItem)) 339 | { 340 | // TODO 341 | } 342 | 343 | // The IOrchestrationService.CompleteTaskActivityWorkItemAsync method 344 | // is expected to set the result for this task. 345 | return workItem.TaskCompletionSource.Task; 346 | } 347 | 348 | #endregion 349 | 350 | IWorkflowActor GetOrchestrationActorProxy(string instanceId, TimeSpan? timeout = null) 351 | { 352 | // REVIEW: Should we be caching these proxy objects? 353 | return ActorProxy.Create( 354 | new ActorId(instanceId), 355 | nameof(WorkflowActor), 356 | new ActorProxyOptions { RequestTimeout = timeout }); 357 | } 358 | 359 | class WorkflowExecutionWorkItem : TaskOrchestrationWorkItem 360 | { 361 | public TaskCompletionSource TaskCompletionSource { get; } = new(); 362 | } 363 | 364 | class ActivityExecutionWorkItem : TaskActivityWorkItem 365 | { 366 | public TaskCompletionSource TaskCompletionSource { get; } = new(); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/DurableTask.Dapr.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | Microsoft.DurableTask.Dapr 12 | Dapr state provider for the Durable Task Sidecar 13 | Dapr backend state provider implementation for the Durable Task Framework. 14 | true 15 | MIT 16 | $(RepositoryUrl) 17 | true 18 | true 19 | https://github.com/cgillum/durabletask-dapr 20 | 0.1.1 21 | alpha 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Helpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | 7 | namespace DurableTask.Dapr; 8 | 9 | static class Helpers 10 | { 11 | public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) 12 | { 13 | if (items.TryGetNonEnumeratedCount(out int count) && count == 0) 14 | { 15 | return; 16 | } 17 | 18 | static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) 19 | { 20 | await semaphore.WaitAsync(); 21 | try 22 | { 23 | await action(item); 24 | } 25 | finally 26 | { 27 | semaphore.Release(); 28 | } 29 | } 30 | 31 | using var semaphore = new SemaphoreSlim(maxConcurrency); 32 | List tasks = count > 0 ? new(count) : new(); 33 | foreach (T item in items) 34 | { 35 | tasks.Add(InvokeThrottledAction(item, action, semaphore)); 36 | } 37 | 38 | await Task.WhenAll(tasks); 39 | } 40 | 41 | public static Task ParallelForEachAsync(this IEnumerable items, Func action) 42 | { 43 | // Choosing a max concurrency is a tradeoff between throughput and the overhead of thread creation. 44 | // A conservative value of 4 feels like a safe default. 45 | return ParallelForEachAsync(items, maxConcurrency: 4, action); 46 | } 47 | 48 | public static ILogger CreateLoggerForDaprProvider(this ILoggerFactory? factory) 49 | { 50 | return factory?.CreateLogger("DurableTask.Dapr") ?? NullLogger.Instance; 51 | } 52 | 53 | public static TimeSpan PositiveOrZero(this TimeSpan timeSpan) 54 | { 55 | return timeSpan > TimeSpan.Zero ? timeSpan : TimeSpan.Zero; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Logs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Dapr.Actors; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace DurableTask.Dapr; 8 | 9 | /// 10 | /// Extension methods for that write Dapr Workflow-specific logs. 11 | /// 12 | static partial class Logs 13 | { 14 | // NOTE: All partial methods defined in this class have source-generated implementations. 15 | 16 | [LoggerMessage( 17 | EventId = 1, 18 | Level = LogLevel.Information, 19 | Message = "{actorId}: Creating reminder '{reminderName}' with due time {dueTime} and recurrence {recurrence}.")] 20 | public static partial void CreatingReminder( 21 | this ILogger logger, 22 | ActorId actorId, 23 | string reminderName, 24 | TimeSpan dueTime, 25 | TimeSpan recurrence); 26 | 27 | [LoggerMessage( 28 | EventId = 2, 29 | Level = LogLevel.Information, 30 | Message = "{actorId}: Reminder '{reminderName}' fired.")] 31 | public static partial void ReminderFired( 32 | this ILogger logger, 33 | ActorId actorId, 34 | string reminderName); 35 | 36 | [LoggerMessage( 37 | EventId = 3, 38 | Level = LogLevel.Information, 39 | Message = "{actorId}: Fetching workflow state.")] 40 | public static partial void FetchingWorkflowState(this ILogger logger, ActorId actorId); 41 | 42 | [LoggerMessage( 43 | EventId = 4, 44 | Level = LogLevel.Warning, 45 | Message = "{actorId}: No workflow state was found to handle reminder '{reminderName}'.")] 46 | public static partial void WorkflowStateNotFound( 47 | this ILogger logger, 48 | ActorId actorId, 49 | string reminderName); 50 | 51 | [LoggerMessage( 52 | EventId = 5, 53 | Level = LogLevel.Warning, 54 | Message = "{instanceId}: No durable timer entry was found that matched reminder '{reminderName}'.")] 55 | public static partial void TimerNotFound( 56 | this ILogger logger, 57 | string instanceId, 58 | string reminderName); 59 | 60 | [LoggerMessage( 61 | EventId = 6, 62 | Level = LogLevel.Warning, 63 | Message = "{instanceId}: A reminder '{reminderName}' was triggered for this workflow but the workflow inbox was empty.")] 64 | public static partial void InboxIsEmpty( 65 | this ILogger logger, 66 | string instanceId, 67 | string reminderName); 68 | 69 | [LoggerMessage( 70 | EventId = 7, 71 | Level = LogLevel.Information, 72 | Message = "{instanceId}: Scheduling {count} activity tasks: {taskList}")] 73 | public static partial void SchedulingActivityTasks( 74 | this ILogger logger, 75 | string instanceId, 76 | int count, 77 | string taskList); 78 | 79 | [LoggerMessage( 80 | EventId = 8, 81 | Level = LogLevel.Warning, 82 | Message = "{actorId}: Received invalid activity batch request.")] 83 | public static partial void InvalidActivityBatchRequest(this ILogger logger, string actorId); 84 | 85 | [LoggerMessage( 86 | EventId = 9, 87 | Level = LogLevel.Information, 88 | Message = "{actorId}: Received activity batch request #{sequenceNumber} for '{instanceId}' with {count} activity tasks: {taskList}")] 89 | public static partial void ReceivedActivityBatchRequest( 90 | this ILogger logger, 91 | string actorId, 92 | string instanceId, 93 | int sequenceNumber, 94 | int count, 95 | string taskList); 96 | 97 | [LoggerMessage( 98 | EventId = 10, 99 | Level = LogLevel.Warning, 100 | Message = "{actorId}: {details}.")] 101 | public static partial void ActivityActorWarning( 102 | this ILogger logger, 103 | ActorId actorId, 104 | string details); 105 | 106 | [LoggerMessage( 107 | EventId = 11, 108 | Level = LogLevel.Information, 109 | Message = "{actorId}: Deleting reminder '{reminderName}'.")] 110 | public static partial void DeletingReminder( 111 | this ILogger logger, 112 | ActorId actorId, 113 | string reminderName); 114 | } 115 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/OrchestrationServiceBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using DurableTask.Core; 5 | using DurableTask.Core.History; 6 | 7 | namespace DurableTask.Dapr; 8 | 9 | public abstract class OrchestrationServiceBase : IOrchestrationService, IOrchestrationServiceClient 10 | { 11 | CancellationTokenSource? shutdownTokenSource; 12 | 13 | /// 14 | /// Gets a that can be used to react to shutdown events. 15 | /// 16 | protected CancellationToken ShutdownToken => this.shutdownTokenSource?.Token ?? CancellationToken.None; 17 | 18 | /// 19 | /// Gets the number of concurrent orchestration dispatchers for fetching orchestration work items. 20 | /// 21 | public virtual int TaskOrchestrationDispatcherCount => 1; 22 | 23 | /// 24 | /// Gets the number of concurrent activity dispatchers for fetching activity work items. 25 | /// 26 | public virtual int TaskActivityDispatcherCount => 1; 27 | 28 | public virtual int MaxConcurrentTaskOrchestrationWorkItems 29 | => Environment.ProcessorCount; 30 | 31 | public virtual int MaxConcurrentTaskActivityWorkItems 32 | => Environment.ProcessorCount; 33 | 34 | public virtual BehaviorOnContinueAsNew EventBehaviourForContinueAsNew 35 | => BehaviorOnContinueAsNew.Carryover; 36 | 37 | public virtual Task CreateAsync() 38 | => this.CreateAsync(recreateInstanceStore: false); 39 | 40 | public abstract Task CreateIfNotExistsAsync(); 41 | 42 | public abstract Task CreateAsync(bool recreateInstanceStore); 43 | 44 | public virtual Task StartAsync() 45 | { 46 | this.shutdownTokenSource?.Dispose(); 47 | this.shutdownTokenSource = new CancellationTokenSource(); 48 | return Task.CompletedTask; 49 | } 50 | 51 | public virtual Task StopAsync() => this.StopAsync(isForced: false); 52 | 53 | public virtual Task StopAsync(bool isForced) 54 | { 55 | this.shutdownTokenSource?.Cancel(); 56 | return Task.CompletedTask; 57 | } 58 | 59 | public virtual Task DeleteAsync() 60 | => this.DeleteAsync(deleteInstanceStore: true); 61 | 62 | public abstract Task DeleteAsync(bool deleteInstanceStore); 63 | 64 | public abstract Task LockNextTaskOrchestrationWorkItemAsync( 65 | TimeSpan receiveTimeout, 66 | CancellationToken cancellationToken); 67 | 68 | public abstract Task RenewTaskOrchestrationWorkItemLockAsync(TaskOrchestrationWorkItem workItem); 69 | 70 | public virtual bool IsMaxMessageCountExceeded( 71 | int currentMessageCount, 72 | OrchestrationRuntimeState runtimeState) => false; 73 | 74 | public abstract Task CompleteTaskOrchestrationWorkItemAsync( 75 | TaskOrchestrationWorkItem workItem, 76 | OrchestrationRuntimeState newOrchestrationRuntimeState, 77 | IList outboundMessages, 78 | IList orchestratorMessages, 79 | IList timerMessages, 80 | TaskMessage continuedAsNewMessage, 81 | OrchestrationState orchestrationState); 82 | 83 | public virtual Task ReleaseTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) 84 | => Task.CompletedTask; 85 | 86 | public abstract Task AbandonTaskOrchestrationWorkItemAsync( 87 | TaskOrchestrationWorkItem workItem); 88 | 89 | public abstract Task LockNextTaskActivityWorkItem( 90 | TimeSpan receiveTimeout, 91 | CancellationToken cancellationToken); 92 | 93 | public abstract Task RenewTaskActivityWorkItemLockAsync(TaskActivityWorkItem workItem); 94 | 95 | public abstract Task CompleteTaskActivityWorkItemAsync( 96 | TaskActivityWorkItem workItem, 97 | TaskMessage responseMessage); 98 | 99 | public abstract Task AbandonTaskActivityWorkItemAsync( 100 | TaskActivityWorkItem workItem); 101 | 102 | public virtual int GetDelayInSecondsAfterOnFetchException(Exception exception) 103 | { 104 | return exception is OperationCanceledException ? 0 : 1; 105 | } 106 | 107 | public virtual int GetDelayInSecondsAfterOnProcessException(Exception exception) 108 | { 109 | return exception is OperationCanceledException ? 0 : 1; 110 | } 111 | 112 | public virtual Task CreateTaskOrchestrationAsync(TaskMessage creationMessage) 113 | => this.CreateTaskOrchestrationAsync(creationMessage, Array.Empty()); 114 | 115 | public abstract Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses); 116 | 117 | public abstract Task SendTaskOrchestrationMessageAsync(TaskMessage message); 118 | 119 | public virtual Task SendTaskOrchestrationMessageBatchAsync(params TaskMessage[] messages) 120 | => Task.WhenAll(messages.Select(msg => this.SendTaskOrchestrationMessageAsync(msg))); 121 | 122 | public abstract Task WaitForOrchestrationAsync( 123 | string instanceId, 124 | string executionId, 125 | TimeSpan timeout, 126 | CancellationToken cancellationToken); 127 | 128 | public virtual Task ForceTerminateTaskOrchestrationAsync(string instanceId, string reason) 129 | { 130 | var taskMessage = new TaskMessage 131 | { 132 | OrchestrationInstance = new OrchestrationInstance { InstanceId = instanceId }, 133 | Event = new ExecutionTerminatedEvent(-1, reason), 134 | }; 135 | 136 | return this.SendTaskOrchestrationMessageAsync(taskMessage); 137 | } 138 | 139 | public virtual async Task> GetOrchestrationStateAsync(string instanceId, bool allExecutions) 140 | { 141 | OrchestrationState? state = await this.GetOrchestrationStateAsync(instanceId, executionId: null); 142 | if (state == null) 143 | { 144 | return Array.Empty(); 145 | } 146 | 147 | return new[] { state }; 148 | } 149 | 150 | public abstract Task GetOrchestrationStateAsync(string instanceId, string? executionId); 151 | 152 | public virtual Task GetOrchestrationHistoryAsync(string instanceId, string? executionId) 153 | => throw new NotImplementedException(); 154 | 155 | public abstract Task PurgeOrchestrationHistoryAsync( 156 | DateTime thresholdDateTimeUtc, 157 | OrchestrationStateTimeRangeFilterType timeRangeFilterType); 158 | } 159 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/ReliableActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Dapr.Actors.Runtime; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace DurableTask.Dapr; 8 | 9 | /// 10 | /// Base class for actors used to implement reliable workflow execution. 11 | /// 12 | /// 13 | /// All logic that needs to execute reliably is backed by a reminder. If the logic executes successfully, even if the 14 | /// result is an exception, that reminder will be deleted. The reminder is intended to ressurect the execution in the 15 | /// event of an unexpected process failure. 16 | /// 17 | abstract class ReliableActor : Actor, IRemindable 18 | { 19 | readonly DaprOptions options; 20 | 21 | protected ReliableActor(ActorHost host, DaprOptions options) 22 | : base(host) 23 | { 24 | this.Log = options.LoggerFactory.CreateLoggerForDaprProvider(); 25 | this.options = options; 26 | } 27 | 28 | protected ILogger Log { get; } 29 | 30 | async Task IRemindable.ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) 31 | { 32 | this.Log.ReminderFired(this.Id, reminderName); 33 | try 34 | { 35 | await this.OnReminderReceivedAsync(reminderName, state); 36 | } 37 | finally 38 | { 39 | if (period > TimeSpan.Zero) 40 | { 41 | this.Log.DeletingReminder(this.Id, reminderName); 42 | 43 | // NOTE: This doesn't actually delete the reminder: https://github.com/dapr/dapr/issues/4801 44 | await this.UnregisterReminderAsync(reminderName); 45 | } 46 | } 47 | } 48 | 49 | protected Task CreateReliableReminder(string reminderName, byte[]? state = null, TimeSpan? delay = null) 50 | { 51 | TimeSpan recurrence = this.options.ReliableReminderInterval; 52 | if (recurrence <= TimeSpan.Zero) 53 | { 54 | // Disable recurrence 55 | recurrence = TimeSpan.FromMilliseconds(-1); 56 | } 57 | 58 | // The default behavior is to execute immediately 59 | delay ??= TimeSpan.Zero; 60 | 61 | this.Log.CreatingReminder(this.Id, reminderName, delay.Value, recurrence); 62 | return this.RegisterReminderAsync(reminderName, state, delay.Value, recurrence); 63 | } 64 | 65 | protected abstract Task OnReminderReceivedAsync(string reminderName, byte[] state); 66 | } 67 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Workflows/IWorkflowActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Dapr.Actors; 5 | using DurableTask.Core; 6 | using DurableTask.Dapr.Activities; 7 | 8 | namespace DurableTask.Dapr.Workflows; 9 | 10 | /// 11 | /// Interface for interacting with workflow actors. 12 | /// 13 | public interface IWorkflowActor : IActor 14 | { 15 | /// 16 | /// Initializes a workflow actor with an ExecutionStarted message for creating a workflow orchestration instance. 17 | /// 18 | /// The message containing the ExecutionStarted history event. 19 | /// 20 | /// Fail the init operation if an orchestration with one of these status values already exists. 21 | /// 22 | /// A task that completes when the actor has finished initializing. 23 | Task InitAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses); 24 | 25 | /// 26 | /// Returns the current state of the workflow. 27 | /// 28 | Task GetCurrentStateAsync(); 29 | 30 | /// 31 | /// Marks an activity execution as completed. 32 | /// 33 | /// 34 | /// Additional information about the activity completion, such as the ID of the activity task, the result, etc. 35 | /// 36 | /// A task that completes when the activity completion is stored in the workflow state. 37 | Task CompleteActivityAsync(ActivityCompletionResponse completionInfo); 38 | 39 | /// 40 | /// Marks a sub-orchestration execution as completed. 41 | /// 42 | /// The message containing the sub-orchestration completion details. 43 | /// A task that completes when the sub-orchestration completion is successfully recorded. 44 | Task CompleteSubOrchestrationAsync(TaskMessage message); 45 | 46 | /// 47 | /// Posts a message to the workflow's inbox. 48 | /// 49 | /// The message to post. 50 | /// A task that completes when the message is successfully persisted. 51 | Task PostToInboxAsync(TaskMessage message); 52 | } 53 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Workflows/IWorkflowExecutor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using DurableTask.Core; 5 | using DurableTask.Core.History; 6 | 7 | namespace DurableTask.Dapr.Workflows; 8 | 9 | /// 10 | /// Interface for types that are able to execute workflow logic. 11 | /// 12 | public interface IWorkflowExecutor 13 | { 14 | /// 15 | /// Executes the next step in a workflow's logic and returns the results of that execution. 16 | /// 17 | /// The instance ID of the workflow. 18 | /// The set of new events to be processed by the workflow. 19 | /// The existing history state of the workflow. 20 | /// 21 | /// A task that completes when the workflow step execution completes. The result of this task is the actions 22 | /// that were scheduled by the workflow. 23 | /// 24 | Task ExecuteWorkflowStepAsync( 25 | string instanceId, 26 | IList inbox, 27 | IList history); 28 | } 29 | 30 | /// 31 | /// The output of the workflow execution step. 32 | /// 33 | /// The type of the result - e.g. whether the execution ran, got throttled, aborted, etc. 34 | /// The updated orchestration state associated with the workflow instance. 35 | /// Any scheduled timers. 36 | /// Any scheduled activity invocation. 37 | /// Any scheduled orchestration messages (sub-orchestrations, events, etc.). 38 | /// The set of history events to append to the workflow orchestration state. 39 | public record WorkflowExecutionResult( 40 | ExecutionResultType Type, 41 | OrchestrationState UpdatedState, 42 | IList Timers, 43 | IList ActivityOutbox, 44 | IList OrchestrationOutbox, 45 | IList NewHistoryEvents); 46 | 47 | /// 48 | /// Represents the set of outcome types for a workflow execution step. 49 | /// 50 | public enum ExecutionResultType 51 | { 52 | /// 53 | /// The workflow executed successfully. 54 | /// 55 | Executed, 56 | 57 | /// 58 | /// The workflow was unable to run due to concurrency throttles. 59 | /// 60 | Throttled, 61 | 62 | /// 63 | /// There was a problem executing the workflow and the results, if any, should be discarded. 64 | /// The workflow should be retried again later. 65 | /// 66 | Abandoned, 67 | } 68 | -------------------------------------------------------------------------------- /src/DurableTask.Dapr/Workflows/WorkflowActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.ObjectModel; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Runtime.Serialization; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Dapr.Actors; 10 | using Dapr.Actors.Client; 11 | using Dapr.Actors.Runtime; 12 | using Dapr.Client; 13 | using DurableTask.Core; 14 | using DurableTask.Core.History; 15 | using DurableTask.Dapr.Activities; 16 | using Microsoft.Extensions.Logging; 17 | 18 | namespace DurableTask.Dapr.Workflows; 19 | 20 | class WorkflowActor : ReliableActor, IWorkflowActor 21 | { 22 | readonly DaprClient daprClient; 23 | readonly IWorkflowExecutor workflowScheduler; 24 | 25 | WorkflowState? state; 26 | 27 | public WorkflowActor( 28 | ActorHost host, 29 | DaprOptions options, 30 | DaprClient daprClient, 31 | IWorkflowExecutor workflowScheduler) 32 | : base(host, options) 33 | { 34 | this.workflowScheduler = workflowScheduler ?? throw new ArgumentNullException(nameof(workflowScheduler)); 35 | this.daprClient = daprClient ?? throw new ArgumentNullException(nameof(daprClient)); 36 | } 37 | 38 | async Task IWorkflowActor.InitAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses) 39 | { 40 | ConditionalValue existing = await this.TryLoadStateAsync(); 41 | 42 | // NOTE: Validation must happen before the reminder is scheduled. 43 | // De-dupe logic 44 | if (existing.HasValue && dedupeStatuses.Contains(existing.Value.OrchestrationStatus)) 45 | { 46 | // TODO: Use a more specific exception type 47 | throw new InvalidOperationException("This orchestration already exists!"); 48 | } 49 | 50 | if (creationMessage.Event is not ExecutionStartedEvent startEvent) 51 | { 52 | throw new ArgumentException($"Only {nameof(ExecutionStartedEvent)} messages can be used to initialize workflows."); 53 | } 54 | 55 | // Schedule a reminder to execute immediately after this operation. The reminder will trigger the actual 56 | // orchestration execution. This is preferable to using the current thread so that we don't block the client 57 | // while the workflow logic is running. 58 | // 59 | // RELIABILITY: 60 | // This reminder must be scheduled before the state update to ensure that an unexpected process failure 61 | // doesn't result in orphaned state in the state store. 62 | await this.CreateReliableReminder("init", state: null, delay: TimeSpan.Zero); 63 | 64 | // Cache the state in memory so that we don't have to fetch it again when the reminder fires. 65 | DateTime now = DateTime.UtcNow; 66 | this.state = new WorkflowState 67 | { 68 | OrchestrationStatus = OrchestrationStatus.Pending, 69 | Name = startEvent.Name, 70 | Input = startEvent.Input, 71 | InstanceId = startEvent.OrchestrationInstance.InstanceId, 72 | ExecutionId = startEvent.OrchestrationInstance.ExecutionId, 73 | CreatedTimeUtc = now, 74 | LastUpdatedTimeUtc = now, 75 | Inbox = { creationMessage }, 76 | }; 77 | 78 | // Persist the initial "Pending" state to the state store/ 79 | // 80 | // RELIABILITY: 81 | // If a crash occurs before the state is saved, the previously scheduled reminder should fail to find the 82 | // initial workflow state and won't schedule any workflow execution. This is the correct behavior. 83 | await this.StateManager.SetStateAsync("state", this.state); 84 | await this.StateManager.SaveStateAsync(); 85 | } 86 | 87 | protected override async Task OnReminderReceivedAsync(string reminderName, byte[] state) 88 | { 89 | await this.TryLoadStateAsync(); 90 | if (this.state == null) 91 | { 92 | // The actor may have failed to save its state after being created. The client should have already received 93 | // an error message associated with the failure, so we can safely drop this reminder. 94 | this.Log.WorkflowStateNotFound(this.Id, reminderName); 95 | return; 96 | } 97 | 98 | if (IsReminderForDurableTimer(reminderName)) 99 | { 100 | if (this.state.Timers.TryGetValue(reminderName, out TimerFiredEvent? timerEvent)) 101 | { 102 | this.state.Inbox.Add(this.state.NewMessage(timerEvent)); 103 | } 104 | else 105 | { 106 | this.Log.TimerNotFound(this.state.InstanceId, reminderName); 107 | } 108 | } 109 | 110 | if (!this.state.Inbox.Any()) 111 | { 112 | // Nothing to do! This isn't expected. 113 | this.Log.InboxIsEmpty(this.state.InstanceId, reminderName); 114 | return; 115 | } 116 | 117 | this.state.SequenceNumber++; 118 | 119 | WorkflowExecutionResult result = await this.workflowScheduler.ExecuteWorkflowStepAsync( 120 | this.state.InstanceId, 121 | this.state.Inbox, 122 | this.state.History); 123 | 124 | List parallelTasks = new(); 125 | 126 | switch (result.Type) 127 | { 128 | case ExecutionResultType.Executed: 129 | // Persist the changes to the data store 130 | DateTime utcNow = DateTime.UtcNow; 131 | this.state.OrchestrationStatus = result.UpdatedState.OrchestrationStatus; 132 | this.state.LastUpdatedTimeUtc = utcNow; 133 | this.state.Output = result.UpdatedState.Output; 134 | this.state.CustomStatus = result.UpdatedState.Status; 135 | 136 | if (result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Completed || 137 | result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Failed || 138 | result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Terminated) 139 | { 140 | this.state.CompletedTimeUtc = utcNow; 141 | 142 | // Notifying parent orchestrations about sub-orchestration completion 143 | IReadOnlyList subOrchestrationCompleteRequests = result.OrchestrationOutbox 144 | .Where(msg => 145 | msg.Event.EventType == EventType.SubOrchestrationInstanceCompleted || 146 | msg.Event.EventType == EventType.SubOrchestrationInstanceFailed) 147 | .ToList(); 148 | if (subOrchestrationCompleteRequests.Count > 0) 149 | { 150 | await subOrchestrationCompleteRequests.ParallelForEachAsync(async message => 151 | { 152 | // REVIEW: Should we be caching these proxy objects? 153 | IWorkflowActor actor = ActorProxy.Create( 154 | new ActorId(message.OrchestrationInstance.InstanceId), 155 | nameof(WorkflowActor), 156 | new ActorProxyOptions()); 157 | 158 | await actor.CompleteSubOrchestrationAsync(message); 159 | }); 160 | } 161 | } 162 | else 163 | { 164 | // Schedule reminders for durable timers. 165 | // It only makes sense to schedule timers when the orchestration has *not* completed. 166 | parallelTasks.Add(result.Timers.ParallelForEachAsync(item => 167 | { 168 | TimerFiredEvent timer = (TimerFiredEvent)item.Event; 169 | lock (this.state.Timers) 170 | { 171 | this.state.Timers.Add(timer); 172 | } 173 | 174 | TimeSpan reminderDelay = timer.FireAt.Subtract(utcNow).PositiveOrZero(); 175 | return this.CreateReliableReminder(GetReminderNameForTimer(timer), delay: reminderDelay); 176 | })); 177 | } 178 | 179 | // Process outbox messages 180 | IReadOnlyList activityRequests = result.ActivityOutbox 181 | .Where(msg => msg.Event.EventType == EventType.TaskScheduled) 182 | .Select(msg => 183 | { 184 | TaskScheduledEvent taskEvent = (TaskScheduledEvent)msg.Event; 185 | return new ActivityInvocationRequest( 186 | taskEvent.Name!, 187 | taskEvent.EventId, 188 | taskEvent.Input, 189 | msg.OrchestrationInstance.InstanceId, 190 | msg.OrchestrationInstance.ExecutionId); 191 | }) 192 | .ToList(); 193 | 194 | if (activityRequests.Count > 0) 195 | { 196 | this.Log.SchedulingActivityTasks( 197 | this.state.InstanceId, 198 | activityRequests.Count, 199 | string.Join(", ", activityRequests)); 200 | 201 | // Each activity invocation gets triggered in parallel by its own stateless actor. 202 | // The task ID of the activity is used to uniquely identify the activity for this 203 | // particular workflow instance. 204 | parallelTasks.Add(activityRequests.ParallelForEachAsync(async request => 205 | { 206 | IActivityActor activityInvokerProxy = ActorProxy.Create( 207 | new ActorId($"{this.state.InstanceId}:activity:{request.TaskId}"), 208 | nameof(StatelessActivityActor), 209 | new ActorProxyOptions()); 210 | await activityInvokerProxy.InvokeAsync(request); 211 | })); 212 | } 213 | 214 | List subOrchestrationCreateRequests = new(); 215 | List pubSubEvents = new(); 216 | 217 | foreach (TaskMessage message in result.OrchestrationOutbox) 218 | { 219 | if (message.Event.EventType == EventType.ExecutionStarted) 220 | { 221 | subOrchestrationCreateRequests.Add(message); 222 | } 223 | else if (PubSubEvent.TryParse(message, out PubSubEvent? pubSubEvent)) 224 | { 225 | pubSubEvents.Add(pubSubEvent); 226 | } 227 | else 228 | { 229 | this.Log.LogWarning( 230 | "Don't know how to handle {eventType} for orchestrator outputs", 231 | message.Event.EventType); 232 | } 233 | } 234 | 235 | // Creating new sub-orchestration instances 236 | if (subOrchestrationCreateRequests.Count > 0) 237 | { 238 | OrchestrationStatus[] dedupeFilter = new[] 239 | { 240 | OrchestrationStatus.Pending, 241 | OrchestrationStatus.Running, 242 | }; 243 | 244 | parallelTasks.Add(subOrchestrationCreateRequests.ParallelForEachAsync(async message => 245 | { 246 | // REVIEW: Should we be caching these proxy objects? 247 | IWorkflowActor actor = ActorProxy.Create( 248 | new ActorId(message.OrchestrationInstance.InstanceId), 249 | nameof(WorkflowActor), 250 | new ActorProxyOptions()); 251 | 252 | await actor.InitAsync(message, dedupeFilter); 253 | })); 254 | } 255 | 256 | if (pubSubEvents.Count > 0) 257 | { 258 | parallelTasks.Add(pubSubEvents.ParallelForEachAsync(async pubSubEvent => 259 | { 260 | await this.daprClient.PublishEventAsync( 261 | pubSubEvent.PubSubName, 262 | pubSubEvent.Topic, 263 | pubSubEvent.Payload); 264 | })); 265 | } 266 | 267 | if (parallelTasks.Count > 0) 268 | { 269 | await Task.WhenAll(parallelTasks); 270 | } 271 | 272 | // At this point, the inbox should be fully processed. 273 | this.state.Inbox.Clear(); 274 | 275 | // Append the new history 276 | // TODO: Move each history event into its own key for better scalability and reduced I/O (esp. writes). 277 | // Alternatively, key by sequence number to reduce the number of roundtrips when loading state. 278 | this.state.History.AddRange(result.NewHistoryEvents); 279 | 280 | // RELIABILITY: 281 | // Saving state needs to be the last step to ensure that a crash doesn't cause us to lose any work. 282 | // If there is a crash, there should be a reminder that wakes us up and causes us to reprocess the 283 | // latest orchestration state. 284 | // TODO: Need to implement this persistent reminder... 285 | await this.StateManager.SetStateAsync("state", this.state); 286 | await this.StateManager.SaveStateAsync(); 287 | 288 | break; 289 | case ExecutionResultType.Throttled: 290 | // TODO: Exponential backoff with some randomness to avoid thundering herd problem. 291 | await this.CreateReliableReminder("retry", delay: TimeSpan.FromSeconds(30)); 292 | break; 293 | case ExecutionResultType.Abandoned: 294 | await this.CreateReliableReminder("retry", delay: TimeSpan.FromSeconds(5)); 295 | break; 296 | } 297 | 298 | await this.UnregisterReminderAsync(reminderName); 299 | } 300 | 301 | async Task IWorkflowActor.GetCurrentStateAsync() 302 | { 303 | ConditionalValue existing = await this.TryLoadStateAsync(); 304 | if (existing.HasValue) 305 | { 306 | WorkflowState internalState = existing.Value; 307 | return new OrchestrationState 308 | { 309 | CompletedTime = internalState.CompletedTimeUtc.GetValueOrDefault(), 310 | CreatedTime = internalState.CreatedTimeUtc, 311 | FailureDetails = null /* TODO */, 312 | Input = internalState.Input, 313 | LastUpdatedTime = internalState.LastUpdatedTimeUtc, 314 | Name = internalState.Name, 315 | OrchestrationInstance = new OrchestrationInstance 316 | { 317 | InstanceId = internalState.InstanceId, 318 | ExecutionId = internalState.ExecutionId, 319 | }, 320 | OrchestrationStatus = internalState.OrchestrationStatus, 321 | Output = internalState.Output, 322 | ParentInstance = null /* TODO */, 323 | Status = internalState.CustomStatus, 324 | }; 325 | } 326 | 327 | return null; 328 | } 329 | 330 | async Task> TryLoadStateAsync() 331 | { 332 | // Cache hit? 333 | if (this.state != null) 334 | { 335 | return new ConditionalValue(true, this.state); 336 | } 337 | 338 | // Cache miss 339 | this.Log.FetchingWorkflowState(this.Id); 340 | ConditionalValue result = await this.StateManager.TryGetStateAsync("state"); 341 | if (result.HasValue) 342 | { 343 | this.state = result.Value; 344 | } 345 | 346 | return result; 347 | } 348 | 349 | static string GetReminderNameForTimer(TimerFiredEvent e) 350 | { 351 | // The TimerId is unique for every timer event for a particular orchestration in the Durable Task Framework. 352 | // WARNING: Do not change this naming convention since that could put timers out of sync with existing reminders. 353 | return $"timer-{e.TimerId}"; 354 | } 355 | 356 | static bool IsReminderForDurableTimer(string reminderName) 357 | { 358 | return reminderName.StartsWith("timer-"); 359 | } 360 | 361 | // This is the API used for external events, termination, etc. 362 | async Task IWorkflowActor.PostToInboxAsync(TaskMessage message) 363 | { 364 | await this.TryLoadStateAsync(); 365 | if (this.state == null) 366 | { 367 | // TODO: If we want to support Durable Entities, we could allow well formatted external event messages 368 | // to invoke the Init flow if there isn't already state associated with the workflow. 369 | this.Log.WorkflowStateNotFound(this.Id, "post-to-inbox"); 370 | return; 371 | } 372 | 373 | this.state.Inbox.Add(message); 374 | 375 | // Save the state after scheduling the reminder to ensure 376 | await this.StateManager.SetStateAsync("state", this.state); 377 | await this.StateManager.SaveStateAsync(); 378 | 379 | // This reminder will trigger the main workflow loop 380 | await this.CreateReliableReminder("received-inbox-message"); 381 | } 382 | 383 | // This is the callback from the activity worker actor when an activity execution completes 384 | async Task IWorkflowActor.CompleteActivityAsync(ActivityCompletionResponse completionInfo) 385 | { 386 | await this.TryLoadStateAsync(); 387 | if (this.state == null) 388 | { 389 | // The actor may have failed to save its state after being created. The client should have already received 390 | // an error message associated with the failure, so we can safely drop this reminder. 391 | this.Log.WorkflowStateNotFound(this.Id, "activity-callback"); 392 | return; 393 | } 394 | 395 | HistoryEvent historyEvent; 396 | if (completionInfo.FailureDetails == null) 397 | { 398 | historyEvent = new TaskCompletedEvent(-1, completionInfo.TaskId, completionInfo.SerializedResult); 399 | } 400 | else 401 | { 402 | historyEvent = new TaskFailedEvent(-1, completionInfo.TaskId, null, null, completionInfo.FailureDetails); 403 | } 404 | 405 | // TODO: De-dupe any task completion events that we've already seen 406 | 407 | this.state.Inbox.Add(this.state.NewMessage(historyEvent)); 408 | await this.StateManager.SetStateAsync("state", this.state); 409 | await this.StateManager.SaveStateAsync(); 410 | 411 | // This reminder will trigger the main workflow loop 412 | await this.CreateReliableReminder($"task-completed-{completionInfo.TaskId}"); 413 | } 414 | 415 | // This is a callback from another workflow actor when a sub-orchestration completes 416 | async Task IWorkflowActor.CompleteSubOrchestrationAsync(TaskMessage message) 417 | { 418 | await this.TryLoadStateAsync(); 419 | if (this.state == null) 420 | { 421 | // The actor may have failed to save its state after being created. The client should have already received 422 | // an error message associated with the failure, so we can safely drop this reminder. 423 | this.Log.WorkflowStateNotFound(this.Id, "sub-orchestration-callback"); 424 | return; 425 | } 426 | 427 | this.state.Inbox.Add(message); 428 | await this.StateManager.SetStateAsync("state", this.state); 429 | await this.StateManager.SaveStateAsync(); 430 | 431 | // This reminder will trigger the main workflow loop 432 | await this.CreateReliableReminder("task-completed"); 433 | } 434 | 435 | /// 436 | /// A collection of pending durable timers that gets saved into workflow state. 437 | /// 438 | /// 439 | /// This collection contains one record for every durable timer scheduled by a specific orchestration instance. 440 | /// This collection supports O(1) key-based lookups, where the key is the name of the associated reminder in Dapr. 441 | /// 442 | class TimerCollection : KeyedCollection 443 | { 444 | protected override string GetKeyForItem(TimerFiredEvent e) 445 | { 446 | return GetReminderNameForTimer(e); 447 | } 448 | } 449 | 450 | record PubSubEvent(string PubSubName, string Topic, object Payload) 451 | { 452 | public static bool TryParse(TaskMessage msg, [NotNullWhen(true)] out PubSubEvent? pubSubEvent) 453 | { 454 | if (msg.Event is EventSentEvent sendEvent && 455 | Uri.TryCreate(sendEvent.InstanceId, UriKind.Absolute, out Uri? destination) && 456 | string.Equals(destination.Scheme, "dapr.pubsub", StringComparison.OrdinalIgnoreCase)) 457 | { 458 | pubSubEvent = new PubSubEvent(PubSubName: destination.Host, Topic: sendEvent.Name, sendEvent.Input); 459 | return true; 460 | } 461 | 462 | pubSubEvent = null; 463 | return false; 464 | } 465 | } 466 | 467 | class WorkflowState 468 | { 469 | public OrchestrationStatus OrchestrationStatus { get; set; } 470 | public string Name { get; init; } = ""; 471 | public string InstanceId { get; init; } = ""; 472 | public string ExecutionId { get; init; } = ""; 473 | [DataMember(EmitDefaultValue = false)] 474 | public string? Input { get; init; } 475 | [DataMember(EmitDefaultValue = false)] 476 | public string? Output { get; set; } 477 | [DataMember(EmitDefaultValue = false)] 478 | public string? CustomStatus { get; set; } 479 | public DateTime CreatedTimeUtc { get; init; } 480 | public DateTime LastUpdatedTimeUtc { get; set; } 481 | [DataMember(EmitDefaultValue = false)] 482 | public DateTime? CompletedTimeUtc { get; set; } 483 | public List Inbox { get; init; } = new List(); 484 | public TimerCollection Timers { get; init; } = new(); 485 | public List History { get; init; } = new List(); 486 | public int SequenceNumber { get; set; } 487 | 488 | internal TaskMessage NewMessage(HistoryEvent e) 489 | { 490 | return new TaskMessage 491 | { 492 | Event = e, 493 | OrchestrationInstance = new OrchestrationInstance 494 | { 495 | InstanceId = this.InstanceId, 496 | ExecutionId = this.ExecutionId, 497 | }, 498 | }; 499 | } 500 | } 501 | } 502 | 503 | // TODO: Move this into its own file 504 | class DurableTaskHistoryConverter : JsonConverter 505 | { 506 | public override HistoryEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 507 | { 508 | using JsonDocument document = JsonDocument.ParseValue(ref reader); 509 | 510 | JsonElement eventTypeJson; 511 | if (!document.RootElement.TryGetProperty("eventType", out eventTypeJson) && 512 | !document.RootElement.TryGetProperty("EventType", out eventTypeJson)) 513 | { 514 | throw new JsonException($"Couldn't find an 'EventType' or 'eventType' property for a history event!"); 515 | } 516 | 517 | EventType eventType = (EventType)eventTypeJson.GetInt32(); 518 | Type targetType = eventType switch 519 | { 520 | EventType.ExecutionStarted => typeof(ExecutionStartedEvent), 521 | EventType.ExecutionCompleted => typeof(ExecutionCompletedEvent), 522 | EventType.ExecutionFailed => typeof(ExecutionCompletedEvent), 523 | EventType.ExecutionTerminated => typeof(ExecutionTerminatedEvent), 524 | EventType.TaskScheduled => typeof(TaskScheduledEvent), 525 | EventType.TaskCompleted => typeof(TaskCompletedEvent), 526 | EventType.TaskFailed => typeof(TaskFailedEvent), 527 | EventType.SubOrchestrationInstanceCreated => typeof(SubOrchestrationInstanceCreatedEvent), 528 | EventType.SubOrchestrationInstanceCompleted => typeof(SubOrchestrationInstanceCompletedEvent), 529 | EventType.SubOrchestrationInstanceFailed => typeof(SubOrchestrationInstanceFailedEvent), 530 | EventType.TimerCreated => typeof(TimerCreatedEvent), 531 | EventType.TimerFired => typeof(TimerFiredEvent), 532 | EventType.OrchestratorStarted => typeof(OrchestratorStartedEvent), 533 | EventType.OrchestratorCompleted => typeof(OrchestratorCompletedEvent), 534 | EventType.EventSent => typeof(EventSentEvent), 535 | EventType.EventRaised => typeof(EventRaisedEvent), 536 | EventType.ContinueAsNew => typeof(ContinueAsNewEvent), 537 | EventType.GenericEvent => typeof(GenericEvent), 538 | EventType.HistoryState => typeof(HistoryStateEvent), 539 | _ => throw new NotSupportedException(), 540 | }; 541 | 542 | string json = document.RootElement.ToString(); 543 | return (HistoryEvent)Newtonsoft.Json.JsonConvert.DeserializeObject(json, targetType); 544 | } 545 | 546 | public override void Write(Utf8JsonWriter writer, HistoryEvent value, JsonSerializerOptions options) 547 | { 548 | string rawJson = Newtonsoft.Json.JsonConvert.SerializeObject(value, Newtonsoft.Json.Formatting.None); 549 | writer.WriteRawValue(rawJson); 550 | } 551 | } 552 | 553 | class DurableTaskTimerFiredConverter : JsonConverter 554 | { 555 | readonly DurableTaskHistoryConverter innerConverter = new(); 556 | 557 | public override TimerFiredEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 558 | { 559 | return (TimerFiredEvent?)this.innerConverter.Read(ref reader, typeToConvert, options); 560 | } 561 | 562 | public override void Write(Utf8JsonWriter writer, TimerFiredEvent value, JsonSerializerOptions options) 563 | { 564 | this.innerConverter.Write(writer, value, options); 565 | } 566 | } 567 | 568 | -------------------------------------------------------------------------------- /src/Workflows/IWorkflowActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Dapr.Actors; 5 | using DurableTask.Core; 6 | using DurableTask.Dapr.Activities; 7 | 8 | namespace DurableTask.Dapr.Workflows; 9 | 10 | /// 11 | /// Interface for interacting with workflow actors. 12 | /// 13 | public interface IWorkflowActor : IActor 14 | { 15 | /// 16 | /// Initializes a workflow actor with an ExecutionStarted message for creating a workflow orchestration instance. 17 | /// 18 | /// The message containing the ExecutionStarted history event. 19 | /// 20 | /// Fail the init operation if an orchestration with one of these status values already exists. 21 | /// 22 | /// A task that completes when the actor has finished initializing. 23 | Task InitAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses); 24 | 25 | /// 26 | /// Returns the current state of the workflow. 27 | /// 28 | Task GetCurrentStateAsync(); 29 | 30 | /// 31 | /// Marks an activity execution as completed. 32 | /// 33 | /// 34 | /// Additional information about the activity completion, such as the ID of the activity task, the result, etc. 35 | /// 36 | /// A task that completes when the activity completion is stored in the workflow state. 37 | Task CompleteActivityAsync(ActivityCompletionResponse completionInfo); 38 | 39 | /// 40 | /// Marks a sub-orchestration execution as completed. 41 | /// 42 | /// The message containing the sub-orchestration completion details. 43 | /// A task that completes when the sub-orchestration completion is successfully recorded. 44 | Task CompleteSubOrchestrationAsync(TaskMessage message); 45 | 46 | /// 47 | /// Posts a message to the workflow's inbox. 48 | /// 49 | /// The message to post. 50 | /// A task that completes when the message is successfully persisted. 51 | Task PostToInboxAsync(TaskMessage message); 52 | } 53 | -------------------------------------------------------------------------------- /src/Workflows/IWorkflowExecutor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using DurableTask.Core; 5 | using DurableTask.Core.History; 6 | 7 | namespace DurableTask.Dapr.Workflows; 8 | 9 | /// 10 | /// Interface for types that are able to execute workflow logic. 11 | /// 12 | public interface IWorkflowExecutor 13 | { 14 | /// 15 | /// Executes the next step in a workflow's logic and returns the results of that execution. 16 | /// 17 | /// The instance ID of the workflow. 18 | /// The set of new events to be processed by the workflow. 19 | /// The existing history state of the workflow. 20 | /// 21 | /// A task that completes when the workflow step execution completes. The result of this task is the actions 22 | /// that were scheduled by the workflow. 23 | /// 24 | Task ExecuteWorkflowStepAsync( 25 | string instanceId, 26 | IList inbox, 27 | IList history); 28 | } 29 | 30 | /// 31 | /// The output of the workflow execution step. 32 | /// 33 | /// The type of the result - e.g. whether the execution ran, got throttled, aborted, etc. 34 | /// The updated orchestration state associated with the workflow instance. 35 | /// Any scheduled timers. 36 | /// Any scheduled activity invocation. 37 | /// Any scheduled orchestration messages (sub-orchestrations, events, etc.). 38 | /// The set of history events to append to the workflow orchestration state. 39 | public record WorkflowExecutionResult( 40 | ExecutionResultType Type, 41 | OrchestrationState UpdatedState, 42 | IList Timers, 43 | IList ActivityOutbox, 44 | IList OrchestrationOutbox, 45 | IList NewHistoryEvents); 46 | 47 | /// 48 | /// Represents the set of outcome types for a workflow execution step. 49 | /// 50 | public enum ExecutionResultType 51 | { 52 | /// 53 | /// The workflow executed successfully. 54 | /// 55 | Executed, 56 | 57 | /// 58 | /// The workflow was unable to run due to concurrency throttles. 59 | /// 60 | Throttled, 61 | 62 | /// 63 | /// There was a problem executing the workflow and the results, if any, should be discarded. 64 | /// The workflow should be retried again later. 65 | /// 66 | Abandoned, 67 | } 68 | -------------------------------------------------------------------------------- /src/Workflows/WorkflowActor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.ObjectModel; 5 | using System.Runtime.Serialization; 6 | using Dapr.Actors; 7 | using Dapr.Actors.Client; 8 | using Dapr.Actors.Runtime; 9 | using DurableTask.Core; 10 | using DurableTask.Core.History; 11 | using DurableTask.Dapr.Activities; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace DurableTask.Dapr.Workflows; 15 | 16 | class WorkflowActor : ReliableActor, IWorkflowActor 17 | { 18 | readonly IWorkflowExecutor workflowScheduler; 19 | 20 | WorkflowState? state; 21 | 22 | public WorkflowActor(ActorHost host, ILoggerFactory loggerFactory, IWorkflowExecutor workflowScheduler) 23 | : base(host, loggerFactory) 24 | { 25 | this.workflowScheduler = workflowScheduler ?? throw new ArgumentNullException(nameof(workflowScheduler)); 26 | } 27 | 28 | async Task IWorkflowActor.InitAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses) 29 | { 30 | ConditionalValue existing = await this.TryLoadStateAsync(); 31 | 32 | // NOTE: Validation must happen before the reminder is scheduled. 33 | // De-dupe logic 34 | if (existing.HasValue && dedupeStatuses.Contains(existing.Value.OrchestrationStatus)) 35 | { 36 | // TODO: Use a more specific exception type 37 | throw new InvalidOperationException("This orchestration already exists!"); 38 | } 39 | 40 | if (creationMessage.Event is not ExecutionStartedEvent startEvent) 41 | { 42 | throw new ArgumentException($"Only {nameof(ExecutionStartedEvent)} messages can be used to initialize workflows."); 43 | } 44 | 45 | // Schedule a reminder to execute immediately after this operation. The reminder will trigger the actual 46 | // orchestration execution. This is preferable to using the current thread so that we don't block the client 47 | // while the workflow logic is running. 48 | // 49 | // RELIABILITY: 50 | // This reminder must be scheduled before the state update to ensure that an unexpected process failure 51 | // doesn't result in orphaned state in the state store. 52 | await this.CreateReliableReminder("init", state: null, delay: TimeSpan.Zero); 53 | 54 | // Cache the state in memory so that we don't have to fetch it again when the reminder fires. 55 | DateTime now = DateTime.UtcNow; 56 | this.state = new WorkflowState 57 | { 58 | OrchestrationStatus = OrchestrationStatus.Pending, 59 | Name = startEvent.Name, 60 | Input = startEvent.Input, 61 | InstanceId = startEvent.OrchestrationInstance.InstanceId, 62 | ExecutionId = startEvent.OrchestrationInstance.ExecutionId, 63 | CreatedTimeUtc = now, 64 | LastUpdatedTimeUtc = now, 65 | Inbox = { creationMessage }, 66 | }; 67 | 68 | // Persist the initial "Pending" state to the state store/ 69 | // 70 | // RELIABILITY: 71 | // If a crash occurs before the state is saved, the previously scheduled reminder should fail to find the 72 | // initial workflow state and won't schedule any workflow execution. This is the correct behavior. 73 | await this.StateManager.SetStateAsync("state", this.state); 74 | await this.StateManager.SaveStateAsync(); 75 | } 76 | 77 | protected override async Task OnReminderReceivedAsync(string reminderName, byte[] state) 78 | { 79 | await this.TryLoadStateAsync(); 80 | if (this.state == null) 81 | { 82 | // The actor may have failed to save its state after being created. The client should have already received 83 | // an error message associated with the failure, so we can safely drop this reminder. 84 | this.Log.WorkflowStateNotFound(this.Id, reminderName); 85 | return; 86 | } 87 | 88 | if (IsReminderForDurableTimer(reminderName)) 89 | { 90 | if (this.state.Timers.TryGetValue(reminderName, out TimerFiredEvent? timerEvent)) 91 | { 92 | this.state.Inbox.Add(this.state.NewMessage(timerEvent)); 93 | } 94 | else 95 | { 96 | this.Log.TimerNotFound(this.state.InstanceId, reminderName); 97 | } 98 | } 99 | 100 | if (!this.state.Inbox.Any()) 101 | { 102 | // Nothing to do! This isn't expected. 103 | this.Log.InboxIsEmpty(this.state.InstanceId, reminderName); 104 | return; 105 | } 106 | 107 | this.state.SequenceNumber++; 108 | 109 | WorkflowExecutionResult result = await this.workflowScheduler.ExecuteWorkflowStepAsync( 110 | this.state.InstanceId, 111 | this.state.Inbox, 112 | this.state.History); 113 | 114 | switch (result.Type) 115 | { 116 | case ExecutionResultType.Executed: 117 | // Persist the changes to the data store 118 | DateTime utcNow = DateTime.UtcNow; 119 | this.state.OrchestrationStatus = result.UpdatedState.OrchestrationStatus; 120 | this.state.LastUpdatedTimeUtc = utcNow; 121 | this.state.Output = result.UpdatedState.Output; 122 | this.state.CustomStatus = result.UpdatedState.Status; 123 | 124 | if (result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Completed || 125 | result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Failed || 126 | result.UpdatedState.OrchestrationStatus == OrchestrationStatus.Terminated) 127 | { 128 | this.state.CompletedTimeUtc = utcNow; 129 | 130 | // Notifying parent orchestrations about sub-orchestration completion 131 | IReadOnlyList subOrchestrationCompleteRequests = result.OrchestrationOutbox 132 | .Where(msg => 133 | msg.Event.EventType == EventType.SubOrchestrationInstanceCompleted || 134 | msg.Event.EventType == EventType.SubOrchestrationInstanceFailed) 135 | .ToList(); 136 | if (subOrchestrationCompleteRequests.Count > 0) 137 | { 138 | await subOrchestrationCompleteRequests.ParallelForEachAsync(async message => 139 | { 140 | // REVIEW: Should we be caching these proxy objects? 141 | IWorkflowActor actor = ActorProxy.Create( 142 | new ActorId(message.OrchestrationInstance.InstanceId), 143 | nameof(WorkflowActor), 144 | new ActorProxyOptions()); 145 | 146 | await actor.CompleteSubOrchestrationAsync(message); 147 | }); 148 | } 149 | } 150 | else 151 | { 152 | List parallelTasks = new(); 153 | 154 | // Schedule reminders for durable timers 155 | parallelTasks.Add(result.Timers.ParallelForEachAsync(item => 156 | { 157 | TimerFiredEvent timer = (TimerFiredEvent)item.Event; 158 | lock (this.state.Timers) 159 | { 160 | this.state.Timers.Add(timer); 161 | } 162 | 163 | TimeSpan reminderDelay = timer.FireAt.Subtract(utcNow).PositiveOrZero(); 164 | return this.CreateReliableReminder(GetReminderNameForTimer(timer), delay: reminderDelay); 165 | })); 166 | 167 | // Process outbox messages 168 | IReadOnlyList activityRequests = result.ActivityOutbox 169 | .Where(msg => msg.Event.EventType == EventType.TaskScheduled) 170 | .Select(msg => 171 | { 172 | TaskScheduledEvent taskEvent = (TaskScheduledEvent)msg.Event; 173 | return new ActivityInvocationRequest( 174 | taskEvent.Name!, 175 | taskEvent.EventId, 176 | taskEvent.Input, 177 | msg.OrchestrationInstance.InstanceId, 178 | msg.OrchestrationInstance.ExecutionId); 179 | }) 180 | .ToList(); 181 | 182 | if (activityRequests.Count > 0) 183 | { 184 | this.Log.SchedulingActivityTasks( 185 | this.state.InstanceId, 186 | activityRequests.Count, 187 | string.Join(", ", activityRequests)); 188 | 189 | // Each activity invocation gets triggered in parallel by its own stateless actor. 190 | // The task ID of the activity is used to uniquely identify the activity for this 191 | // particular workflow instance. 192 | parallelTasks.Add(activityRequests.ParallelForEachAsync(async request => 193 | { 194 | IActivityActor activityInvokerProxy = ActorProxy.Create( 195 | new ActorId($"{this.state.InstanceId}:activity:{request.TaskId}"), 196 | nameof(StatelessActivityActor), 197 | new ActorProxyOptions()); 198 | await activityInvokerProxy.InvokeAsync(request); 199 | })); 200 | } 201 | 202 | // Creating new sub-orchestration instances 203 | IReadOnlyList subOrchestrationCreateRequests = result.OrchestrationOutbox 204 | .Where(msg => msg.Event.EventType == EventType.ExecutionStarted) 205 | .ToList(); 206 | if (subOrchestrationCreateRequests.Count > 0) 207 | { 208 | OrchestrationStatus[] dedupeFilter = new[] 209 | { 210 | OrchestrationStatus.Pending, 211 | OrchestrationStatus.Running, 212 | }; 213 | 214 | parallelTasks.Add(subOrchestrationCreateRequests.ParallelForEachAsync(async message => 215 | { 216 | // REVIEW: Should we be caching these proxy objects? 217 | IWorkflowActor actor = ActorProxy.Create( 218 | new ActorId(message.OrchestrationInstance.InstanceId), 219 | nameof(WorkflowActor), 220 | new ActorProxyOptions()); 221 | 222 | await actor.InitAsync(message, dedupeFilter); 223 | })); 224 | } 225 | 226 | // TODO: Sending of external events 227 | 228 | if (parallelTasks.Count > 0) 229 | { 230 | await Task.WhenAll(parallelTasks); 231 | } 232 | } 233 | 234 | // At this point, the inbox should be fully processed. 235 | this.state.Inbox.Clear(); 236 | 237 | // Append the new history 238 | // TODO: Move each history event into its own key for better scalability and reduced I/O (esp. writes). 239 | // Alternatively, key by sequence number to reduce the number of roundtrips when loading state. 240 | this.state.History.AddRange(result.NewHistoryEvents); 241 | 242 | // RELIABILITY: 243 | // Saving state needs to be the last step to ensure that a crash doesn't cause us to lose any work. 244 | // If there is a crash, there should be a reminder that wakes us up and causes us to reprocess the 245 | // latest orchestration state. 246 | // TODO: Need to implement this persistent reminder... 247 | await this.StateManager.SetStateAsync("state", this.state); 248 | await this.StateManager.SaveStateAsync(); 249 | 250 | break; 251 | case ExecutionResultType.Throttled: 252 | // TODO: Exponential backoff with some randomness to avoid thundering herd problem. 253 | await this.CreateReliableReminder("retry", delay: TimeSpan.FromSeconds(30)); 254 | break; 255 | case ExecutionResultType.Abandoned: 256 | await this.CreateReliableReminder("retry", delay: TimeSpan.FromSeconds(5)); 257 | break; 258 | } 259 | 260 | await this.UnregisterReminderAsync(reminderName); 261 | } 262 | 263 | async Task IWorkflowActor.GetCurrentStateAsync() 264 | { 265 | ConditionalValue existing = await this.TryLoadStateAsync(); 266 | if (existing.HasValue) 267 | { 268 | WorkflowState internalState = existing.Value; 269 | return new OrchestrationState 270 | { 271 | CompletedTime = internalState.CompletedTimeUtc.GetValueOrDefault(), 272 | CreatedTime = internalState.CreatedTimeUtc, 273 | FailureDetails = null /* TODO */, 274 | Input = internalState.Input, 275 | LastUpdatedTime = internalState.LastUpdatedTimeUtc, 276 | Name = internalState.Name, 277 | OrchestrationInstance = new OrchestrationInstance 278 | { 279 | InstanceId = internalState.InstanceId, 280 | ExecutionId = internalState.ExecutionId, 281 | }, 282 | OrchestrationStatus = internalState.OrchestrationStatus, 283 | Output = internalState.Output, 284 | ParentInstance = null /* TODO */, 285 | Status = internalState.CustomStatus, 286 | }; 287 | } 288 | 289 | return null; 290 | } 291 | 292 | async Task> TryLoadStateAsync() 293 | { 294 | // Cache hit? 295 | if (this.state != null) 296 | { 297 | return new ConditionalValue(true, this.state); 298 | } 299 | 300 | // Cache miss 301 | this.Log.FetchingWorkflowState(this.Id); 302 | ConditionalValue result = await this.StateManager.TryGetStateAsync("state"); 303 | if (result.HasValue) 304 | { 305 | this.state = result.Value; 306 | } 307 | 308 | return result; 309 | } 310 | 311 | static string GetReminderNameForTimer(TimerFiredEvent e) 312 | { 313 | // The TimerId is unique for every timer event for a particular orchestration in the Durable Task Framework. 314 | // WARNING: Do not change this naming convention since that could put timers out of sync with existing reminders. 315 | return $"timer-{e.TimerId}"; 316 | } 317 | 318 | static bool IsReminderForDurableTimer(string reminderName) 319 | { 320 | return reminderName.StartsWith("timer-"); 321 | } 322 | 323 | // This is the API used for external events, termination, etc. 324 | async Task IWorkflowActor.PostToInboxAsync(TaskMessage message) 325 | { 326 | await this.TryLoadStateAsync(); 327 | if (this.state == null) 328 | { 329 | // TODO: If we want to support Durable Entities, we could allow well formatted external event messages 330 | // to invoke the Init flow if there isn't already state associated with the workflow. 331 | this.Log.WorkflowStateNotFound(this.Id, "post-to-inbox"); 332 | return; 333 | } 334 | 335 | this.state.Inbox.Add(message); 336 | 337 | // Save the state after scheduling the reminder to ensure 338 | await this.StateManager.SetStateAsync("state", this.state); 339 | await this.StateManager.SaveStateAsync(); 340 | 341 | // This reminder will trigger the main workflow loop 342 | await this.CreateReliableReminder("received-inbox-message"); 343 | } 344 | 345 | // This is the callback from the activity worker actor when an activity execution completes 346 | async Task IWorkflowActor.CompleteActivityAsync(ActivityCompletionResponse completionInfo) 347 | { 348 | await this.TryLoadStateAsync(); 349 | if (this.state == null) 350 | { 351 | // The actor may have failed to save its state after being created. The client should have already received 352 | // an error message associated with the failure, so we can safely drop this reminder. 353 | this.Log.WorkflowStateNotFound(this.Id, "activity-callback"); 354 | return; 355 | } 356 | 357 | HistoryEvent historyEvent; 358 | if (completionInfo.FailureDetails == null) 359 | { 360 | historyEvent = new TaskCompletedEvent(-1, completionInfo.TaskId, completionInfo.SerializedResult); 361 | } 362 | else 363 | { 364 | historyEvent = new TaskFailedEvent(-1, completionInfo.TaskId, null, null, completionInfo.FailureDetails); 365 | } 366 | 367 | // TODO: De-dupe any task completion events that we've already seen 368 | 369 | this.state.Inbox.Add(this.state.NewMessage(historyEvent)); 370 | await this.StateManager.SetStateAsync("state", this.state); 371 | await this.StateManager.SaveStateAsync(); 372 | 373 | // This reminder will trigger the main workflow loop 374 | await this.CreateReliableReminder("task-completed"); 375 | } 376 | 377 | // This is a callback from another workflow actor when a sub-orchestration completes 378 | async Task IWorkflowActor.CompleteSubOrchestrationAsync(TaskMessage message) 379 | { 380 | await this.TryLoadStateAsync(); 381 | if (this.state == null) 382 | { 383 | // The actor may have failed to save its state after being created. The client should have already received 384 | // an error message associated with the failure, so we can safely drop this reminder. 385 | this.Log.WorkflowStateNotFound(this.Id, "sub-orchestration-callback"); 386 | return; 387 | } 388 | 389 | this.state.Inbox.Add(message); 390 | await this.StateManager.SetStateAsync("state", this.state); 391 | await this.StateManager.SaveStateAsync(); 392 | 393 | // This reminder will trigger the main workflow loop 394 | await this.CreateReliableReminder("task-completed"); 395 | } 396 | 397 | /// 398 | /// A collection of pending durable timers that gets saved into workflow state. 399 | /// 400 | /// 401 | /// This collection contains one record for every durable timer scheduled by a specific orchestration instance. 402 | /// This collection supports O(1) key-based lookups, where the key is the name of the associated reminder in Dapr. 403 | /// 404 | class TimerCollection : KeyedCollection 405 | { 406 | protected override string GetKeyForItem(TimerFiredEvent e) 407 | { 408 | return GetReminderNameForTimer(e); 409 | } 410 | } 411 | 412 | class WorkflowState 413 | { 414 | public OrchestrationStatus OrchestrationStatus { get; set; } 415 | public string Name { get; init; } = ""; 416 | public string InstanceId { get; init; } = ""; 417 | public string ExecutionId { get; init; } = ""; 418 | [DataMember(EmitDefaultValue = false)] 419 | public string? Input { get; init; } 420 | [DataMember(EmitDefaultValue = false)] 421 | public string? Output { get; set; } 422 | [DataMember(EmitDefaultValue = false)] 423 | public string? CustomStatus { get; set; } 424 | public DateTime CreatedTimeUtc { get; init; } 425 | public DateTime LastUpdatedTimeUtc { get; set; } 426 | [DataMember(EmitDefaultValue = false)] 427 | public DateTime? CompletedTimeUtc { get; set; } 428 | public List Inbox { get; } = new List(); 429 | public TimerCollection Timers { get; } = new(); 430 | public List History { get; } = new List(); 431 | public int SequenceNumber { get; set; } 432 | 433 | internal TaskMessage NewMessage(HistoryEvent e) 434 | { 435 | return new TaskMessage 436 | { 437 | Event = e, 438 | OrchestrationInstance = new OrchestrationInstance 439 | { 440 | InstanceId = this.InstanceId, 441 | ExecutionId = this.ExecutionId, 442 | }, 443 | }; 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /test/DurableTask.Dapr.Tests/DurableTask.Dapr.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/ManualTesting/ManualTesting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/ManualTesting/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.CommandLine; 3 | using Dapr.Actors; 4 | using Dapr.Actors.Runtime; 5 | using DurableTask.Core; 6 | using DurableTask.Dapr; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace DaprTesting; 10 | 11 | enum Test 12 | { 13 | Echo, 14 | Sleep, 15 | HelloCities, 16 | } 17 | 18 | static class Program 19 | { 20 | static int Main(string[] args) 21 | { 22 | Option testOption = new("--test", "The test to run."); 23 | RootCommand rootCommand = new("Dapr Workflow POC test driver"); 24 | rootCommand.AddOption(testOption); 25 | rootCommand.SetHandler(RunTest, testOption); 26 | return rootCommand.Invoke(args); 27 | } 28 | 29 | static async Task RunTest(Test test) 30 | { 31 | Console.WriteLine("Starting up..."); 32 | 33 | // Write internal log messages to the console. 34 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder => 35 | { 36 | builder.AddSimpleConsole(options => 37 | { 38 | options.SingleLine = true; 39 | options.UseUtcTimestamp = true; 40 | options.TimestampFormat = "yyyy-MM-ddThh:mm:ss.ffffffZ "; 41 | }); 42 | 43 | builder.AddFilter("DurableTask", LogLevel.Information); 44 | 45 | // ASP.NET Core logs to warning since they can otherwise be noisy. 46 | // This should be increased if it's necessary to debug interactions with Dapr. 47 | builder.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); 48 | }); 49 | DaprOptions options = new() { LoggerFactory = loggerFactory }; 50 | 51 | string? reminderIntervalConfig = Environment.GetEnvironmentVariable("DAPR_WORKFLOW_REMINDER_INTERVAL_MINUTES"); 52 | if (int.TryParse(reminderIntervalConfig, out int reminderIntervalMinutes) && reminderIntervalMinutes > 0) 53 | { 54 | options.ReliableReminderInterval = TimeSpan.FromMinutes(reminderIntervalMinutes); 55 | } 56 | 57 | DaprOrchestrationService service = new(options); 58 | 59 | // This is currently a no-op, but we do it anyways since that's the pattern 60 | // DTFx apps are normally supposed to follow. 61 | await service.CreateIfNotExistsAsync(); 62 | 63 | // Register a very simple orchestration and start the worker. 64 | TaskHubWorker worker = new(service, loggerFactory) 65 | { 66 | // This setting is required for failures to work correctly 67 | ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails, 68 | }; 69 | 70 | worker.AddTaskOrchestrations( 71 | typeof(EchoOrchestration), 72 | typeof(SleepOrchestration), 73 | typeof(HelloCitiesOrchestration)); 74 | 75 | worker.AddTaskActivities( 76 | typeof(SayHello)); 77 | 78 | await worker.StartAsync(); 79 | 80 | // Need to give time for the actor runtime to finish initializing before we can safely communicate with them. 81 | Thread.Sleep(TimeSpan.FromSeconds(5)); 82 | 83 | Console.WriteLine($"Press [ENTER] to create a new {test} workflow instance."); 84 | Console.ReadLine(); 85 | 86 | TaskHubClient client = new(service, null, loggerFactory); 87 | 88 | OrchestrationInstance instance; 89 | switch (test) 90 | { 91 | case Test.Echo: 92 | instance = await client.CreateOrchestrationInstanceAsync( 93 | typeof(EchoOrchestration), 94 | input: "Hello, Dapr!"); 95 | break; 96 | case Test.Sleep: 97 | instance = await client.CreateOrchestrationInstanceAsync( 98 | typeof(SleepOrchestration), 99 | input: TimeSpan.FromSeconds(10)); 100 | break; 101 | case Test.HelloCities: 102 | instance = await client.CreateOrchestrationInstanceAsync( 103 | typeof(HelloCitiesOrchestration), 104 | input: null); 105 | break; 106 | default: 107 | Console.Error.WriteLine($"Unknown test type '{test}'."); 108 | return -1; 109 | } 110 | 111 | Console.WriteLine($"Started orchestration with ID = '{instance.InstanceId}' and waiting for it to complete..."); 112 | 113 | OrchestrationState state = await client.WaitForOrchestrationAsync(instance, TimeSpan.FromMinutes(5)); 114 | Console.WriteLine($"Orchestration {state.OrchestrationStatus}! Raw output: {state.Output}"); 115 | 116 | Console.WriteLine("Press [ENTER] to exit."); 117 | Console.ReadLine(); 118 | return 0; 119 | } 120 | } 121 | 122 | /// 123 | /// Simple orchestration that just saves the input as the output. 124 | /// 125 | class EchoOrchestration : TaskOrchestration 126 | { 127 | public override Task RunTask(OrchestrationContext context, string input) 128 | { 129 | return Task.FromResult(input); 130 | } 131 | } 132 | 133 | /// 134 | /// Simple orchestration that sleeps for a given number of seconds. 135 | /// 136 | class SleepOrchestration : TaskOrchestration 137 | { 138 | public override Task RunTask(OrchestrationContext context, TimeSpan delayInput) 139 | { 140 | return context.CreateTimer(context.CurrentUtcDateTime.Add(delayInput), delayInput); 141 | } 142 | } 143 | 144 | class HelloCitiesOrchestration : TaskOrchestration 145 | { 146 | public override async Task RunTask(OrchestrationContext context, string input) 147 | { 148 | string result = ""; 149 | result += await context.ScheduleTask(typeof(SayHello), "Tokyo") + " "; 150 | result += await context.ScheduleTask(typeof(SayHello), "London") + " "; 151 | result += await context.ScheduleTask(typeof(SayHello), "Seattle"); 152 | return result; 153 | } 154 | } 155 | 156 | class SayHello : TaskActivity 157 | { 158 | protected override string Execute(TaskContext context, string input) 159 | { 160 | return $"Hello, {input}!"; 161 | } 162 | } 163 | 164 | // This is just for ad-hoc testing. It's not actually part of this project in any way. 165 | public interface IMyActor : IActor 166 | { 167 | Task DoSomethingAsync(); 168 | } 169 | 170 | public class MyActor : Actor, IMyActor, IRemindable 171 | { 172 | public MyActor(ActorHost host) 173 | : base(host) 174 | { } 175 | 176 | async Task IMyActor.DoSomethingAsync() 177 | { 178 | // Deliver a reminder notification every 5 seconds 179 | await this.RegisterReminderAsync( 180 | "MyReminder", 181 | null, 182 | TimeSpan.FromSeconds(5), 183 | TimeSpan.FromSeconds(5)); 184 | } 185 | 186 | Task IRemindable.ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) 187 | { 188 | this.Logger.LogWarning("[{timestamp}] Reminder {reminderName} was called!", DateTime.UtcNow.ToString("o"), reminderName); 189 | return Task.CompletedTask; 190 | } 191 | } -------------------------------------------------------------------------------- /test/ManualTesting/README.md: -------------------------------------------------------------------------------- 1 | # Manually testing the Dapr Workflow engine POC 2 | 3 | This README walks through the process of manually testing the Dapr Workflow engine POC (proof-of-concept). This guide was originally written for Dapr 1.7. 4 | 5 | ## Running the test 6 | 7 | The `/test/DaprTesting` directory includes a console app that runs a Durable Task Framework orchestration using the Dapr backend. It's designed to be opened in Visual Studio and started with "F5". 8 | 9 | However, before the test can run, you need to start the Dapr sidecar. The best way to set this up is using the Dapr CLI (see the [Dapr getting started guide](https://docs.dapr.io/getting-started/)). Below is the command-line you can use to start the sidecar. 10 | 11 | ```bash 12 | dapr run --app-id myapp2 --dapr-http-port 3500 --app-port 5000 --dapr-grpc-port 50001 13 | ``` 14 | 15 | The output should look something like the following: 16 | 17 | ``` 18 | Starting up... 19 | 2022-06-20T11:46:30.965669Z info: DurableTask.Core[11] Durable task hub worker is starting 20 | 2022-06-20T11:46:31.154072Z info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5000 21 | 2022-06-20T11:46:31.155023Z info: Microsoft.Hosting.Lifetime[14] Now listening on: https://localhost:5001 22 | 2022-06-20T11:46:31.156860Z info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. 23 | 2022-06-20T11:46:31.157933Z info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production 24 | 2022-06-20T11:46:31.157960Z info: Microsoft.Hosting.Lifetime[0] Content root path: C:\GitHub\durabletask-dapr\test\DaprTesting\bin\Debug\net6.0\ 25 | 2022-06-20T11:46:31.202991Z info: DurableTask.Core[11] Durable task hub worker started successfully after 201ms 26 | Press [ENTER] to create an orchestration instance. 27 | ``` 28 | 29 | Press the [Enter] key to start a workflow that executes three activities in sequence. The debug output should look something like the following: 30 | 31 | ``` 32 | 2022-06-20T11:46:39.196304Z info: DurableTask.Core[40] Scheduling orchestration 'DaprTesting.HelloCitiesOrchestration' with instance ID = '03c538d0603f4f3e942995eaf00503f9' and 0 bytes of input 33 | 2022-06-20T11:46:39.603370Z info: DurableTask.Dapr[3] 03c538d0603f4f3e942995eaf00503f9: Fetching workflow state. 34 | 2022-06-20T11:46:39.763185Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9: Creating reminder 'init' with due time 00:00:00 and recurrence -00:00:00.0010000. 35 | Started orchestration with ID = '03c538d0603f4f3e942995eaf00503f9' and waiting for it to complete... 36 | 2022-06-20T11:46:40.020441Z info: DurableTask.Core[43] Waiting up to 300 seconds for instance '03c538d0603f4f3e942995eaf00503f9' to complete, fail, or be terminated 37 | 2022-06-20T11:46:40.083354Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9: Reminder 'init' fired. 38 | 2022-06-20T11:46:40.112238Z info: DurableTask.Core[51] 03c538d0603f4f3e942995eaf00503f9: Executing 'DaprTesting.HelloCitiesOrchestration' orchestration logic 39 | 2022-06-20T11:46:40.262931Z info: DurableTask.Core[52] 03c538d0603f4f3e942995eaf00503f9: Orchestration 'DaprTesting.HelloCitiesOrchestration' awaited and scheduled 1 durable operation(s). 40 | 2022-06-20T11:46:40.264729Z info: DurableTask.Core[46] 03c538d0603f4f3e942995eaf00503f9: Scheduling activity [DaprTesting.SayHello#0] with 0 bytes of input 41 | 2022-06-20T11:46:40.272677Z info: DurableTask.Dapr[7] 03c538d0603f4f3e942995eaf00503f9: Scheduling 1 activity tasks: DaprTesting.SayHello#0 42 | 2022-06-20T11:46:40.361957Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9:activity:0: Creating reminder 'execute' with due time 00:00:00 and recurrence -00:00:00.0010000. 43 | 2022-06-20T11:46:40.436473Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9:activity:0: Reminder 'execute' fired. 44 | 2022-06-20T11:46:40.554816Z info: DurableTask.Core[60] 03c538d0603f4f3e942995eaf00503f9: Starting task activity [DaprTesting.SayHello#0] 45 | 2022-06-20T11:46:40.599043Z info: DurableTask.Core[61] 03c538d0603f4f3e942995eaf00503f9: Task activity [DaprTesting.SayHello#0] completed successfully 46 | 2022-06-20T11:46:40.636674Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9: Creating reminder 'task-completed' with due time 00:00:00 and recurrence -00:00:00.0010000. 47 | 2022-06-20T11:46:40.696998Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9: Reminder 'task-completed' fired. 48 | 2022-06-20T11:46:40.698480Z info: DurableTask.Core[51] 03c538d0603f4f3e942995eaf00503f9: Executing 'DaprTesting.HelloCitiesOrchestration' orchestration logic 49 | 2022-06-20T11:46:40.715871Z info: DurableTask.Core[52] 03c538d0603f4f3e942995eaf00503f9: Orchestration 'DaprTesting.HelloCitiesOrchestration' awaited and scheduled 1 durable operation(s). 50 | 2022-06-20T11:46:40.715944Z info: DurableTask.Core[46] 03c538d0603f4f3e942995eaf00503f9: Scheduling activity [DaprTesting.SayHello#1] with 0 bytes of input 51 | 2022-06-20T11:46:40.716053Z info: DurableTask.Dapr[7] 03c538d0603f4f3e942995eaf00503f9: Scheduling 1 activity tasks: DaprTesting.SayHello#1 52 | 2022-06-20T11:46:40.741876Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9:activity:1: Creating reminder 'execute' with due time 00:00:00 and recurrence -00:00:00.0010000. 53 | 2022-06-20T11:46:40.783975Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9:activity:1: Reminder 'execute' fired. 54 | 2022-06-20T11:46:40.798599Z info: DurableTask.Core[60] 03c538d0603f4f3e942995eaf00503f9: Starting task activity [DaprTesting.SayHello#1] 55 | 2022-06-20T11:46:40.799441Z info: DurableTask.Core[61] 03c538d0603f4f3e942995eaf00503f9: Task activity [DaprTesting.SayHello#1] completed successfully 56 | 2022-06-20T11:46:40.900538Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9: Creating reminder 'task-completed' with due time 00:00:00 and recurrence -00:00:00.0010000. 57 | 2022-06-20T11:46:40.935004Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9: Reminder 'task-completed' fired. 58 | 2022-06-20T11:46:40.949429Z info: DurableTask.Core[51] 03c538d0603f4f3e942995eaf00503f9: Executing 'DaprTesting.HelloCitiesOrchestration' orchestration logic 59 | 2022-06-20T11:46:40.952257Z info: DurableTask.Core[52] 03c538d0603f4f3e942995eaf00503f9: Orchestration 'DaprTesting.HelloCitiesOrchestration' awaited and scheduled 1 durable operation(s). 60 | 2022-06-20T11:46:40.952293Z info: DurableTask.Core[46] 03c538d0603f4f3e942995eaf00503f9: Scheduling activity [DaprTesting.SayHello#2] with 0 bytes of input 61 | 2022-06-20T11:46:40.952327Z info: DurableTask.Dapr[7] 03c538d0603f4f3e942995eaf00503f9: Scheduling 1 activity tasks: DaprTesting.SayHello#2 62 | 2022-06-20T11:46:40.973304Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9:activity:2: Creating reminder 'execute' with due time 00:00:00 and recurrence -00:00:00.0010000. 63 | 2022-06-20T11:46:41.034533Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9:activity:2: Reminder 'execute' fired. 64 | 2022-06-20T11:46:41.036540Z info: DurableTask.Core[60] 03c538d0603f4f3e942995eaf00503f9: Starting task activity [DaprTesting.SayHello#2] 65 | 2022-06-20T11:46:41.036611Z info: DurableTask.Core[61] 03c538d0603f4f3e942995eaf00503f9: Task activity [DaprTesting.SayHello#2] completed successfully 66 | 2022-06-20T11:46:41.146714Z info: DurableTask.Dapr[1] 03c538d0603f4f3e942995eaf00503f9: Creating reminder 'task-completed' with due time 00:00:00 and recurrence -00:00:00.0010000. 67 | 2022-06-20T11:46:41.192156Z info: DurableTask.Dapr[2] 03c538d0603f4f3e942995eaf00503f9: Reminder 'task-completed' fired. 68 | 2022-06-20T11:46:41.192773Z info: DurableTask.Core[51] 03c538d0603f4f3e942995eaf00503f9: Executing 'DaprTesting.HelloCitiesOrchestration' orchestration logic 69 | 2022-06-20T11:46:41.196710Z info: DurableTask.Core[52] 03c538d0603f4f3e942995eaf00503f9: Orchestration 'DaprTesting.HelloCitiesOrchestration' awaited and scheduled 1 durable operation(s). 70 | 2022-06-20T11:46:41.199325Z info: DurableTask.Core[49] 03c538d0603f4f3e942995eaf00503f9: Orchestration completed with a 'Completed' status and 46 bytes of output. Details: 71 | Orchestration Completed! Raw output: "Hello, Tokyo! Hello, London! Hello, Seattle!" 72 | Press [ENTER] to exit. 73 | ``` 74 | 75 | ## Viewing the workflow state in Redis 76 | 77 | Assuming you're using Redis as the actor state store (the default configuration), you can use Redis commands to query the state. The simplest way to do this is to open a bash shell in the redis Docker container and issue commands from an interactive redis CLI. More details on interacting with Redis state stores can be found [here](https://docs.dapr.io/developing-applications/building-blocks/state-management/query-state-store/query-redis-store/). 78 | 79 | Creating a Redis CLI terminal session can be done with the following command. Note that `dapr_redis` is the default name of the Redis container. You can change this value if your redis container has a different name. 80 | 81 | ```bash 82 | docker run --rm -it --link dapr_redis redis redis-cli -h dapr_redis 83 | ``` 84 | 85 | Querying for all workflow instances: 86 | 87 | ```bash 88 | KEYS myapp* 89 | ``` 90 | 91 | You can use the `KEYS myapp*` command to list all workflow instances. The following shows the output when three workflows have been created: 92 | 93 | ``` 94 | dapr_redis:6379> KEYS myapp* 95 | 1) "myapp2||WorkflowActor||96eb655fb257481bb7c0d7b6a64b2622||state" 96 | 2) "myapp2||WorkflowActor||9a66a0d88d29493482f47e24993da8a9||state" 97 | 3) "myapp2||WorkflowActor||03c538d0603f4f3e942995eaf00503f9||state" 98 | ``` 99 | 100 | These keys correspond to Redis hash values. You can query the values using the `HGET {key} data` command where `{key}` is one of the keys listed in the previous `KEYS` command. 101 | 102 | ``` 103 | dapr_redis:6379> HGET "myapp2||WorkflowActor||03c538d0603f4f3e942995eaf00503f9||state" data 104 | "{\"orchestrationStatus\":1,\"instanceId\":\"03c538d0603f4f3e942995eaf00503f9\",\"input\":null,\"customStatus\":null,\"inbox\":[],\"history\":[{\"eventId\":-1,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:40.1086205Z\",\"eventType\":12,\"extensionData\":null},{\"eventType\":0,\"extensionData\":{},\"eventId\":-1,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:39.1952429Z\"},{\"eventId\":0,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.2641071Z\",\"eventType\":4,\"extensionData\":null},{\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.2651619Z\",\"eventType\":13,\"extensionData\":null,\"eventId\":-1},{\"eventId\":-1,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:40.6980195Z\",\"eventType\":12,\"extensionData\":null},{\"timestamp\":\"2022-06-20T23:46:40.6365553Z\",\"eventType\":5,\"extensionData\":null,\"eventId\":-1,\"isPlayed\":true},{\"eventId\":1,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.7159287Z\",\"eventType\":4,\"extensionData\":null},{\"eventType\":13,\"extensionData\":null,\"eventId\":-1,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.7159729Z\"},{\"eventId\":-1,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:40.9493094Z\",\"eventType\":12,\"extensionData\":null},{\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.9005211Z\",\"eventType\":5,\"extensionData\":null,\"eventId\":-1},{\"eventType\":4,\"extensionData\":null,\"eventId\":2,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.9522903Z\"},{\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:40.9522987Z\",\"eventType\":13,\"extensionData\":null,\"eventId\":-1},{\"eventType\":12,\"extensionData\":null,\"eventId\":-1,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:41.1927511Z\"},{\"eventId\":-1,\"isPlayed\":true,\"timestamp\":\"2022-06-20T23:46:41.146698Z\",\"eventType\":5,\"extensionData\":null},{\"eventId\":3,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:41.1985663Z\",\"eventType\":1,\"extensionData\":null},{\"eventId\":-1,\"isPlayed\":false,\"timestamp\":\"2022-06-20T23:46:41.2005877Z\",\"eventType\":13,\"extensionData\":null}],\"name\":\"DaprTesting.HelloCitiesOrchestration\",\"createdTimeUtc\":\"2022-06-20T23:46:39.8561888Z\",\"lastUpdatedTimeUtc\":\"2022-06-20T23:46:41.20076Z\",\"completedTimeUtc\":\"2022-06-20T23:46:41.20076Z\",\"executionId\":\"dede42cbb8b345cba8857a221265a60d\",\"output\":\"\\\"Hello, Tokyo! Hello, London! Hello, Seattle!\\\"\",\"timers\":[],\"sequenceNumber\":4}" 105 | ``` 106 | 107 | ## Clearing workflow state in Redis 108 | 109 | To delete all workflow state is redis, simply delete all the data in the redis DB (WARNING: This deletes all state, not just workflow state). 110 | 111 | ``` 112 | dapr_redis:6379> FLUSHDB 113 | ``` --------------------------------------------------------------------------------