|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +namespace DurableTask.SqlServer |
| 5 | +{ |
| 6 | + using System; |
| 7 | + using System.Threading; |
| 8 | + using System.Threading.Tasks; |
| 9 | + using System.Linq; |
| 10 | + using DurableTask.Core; |
| 11 | + using DurableTask.Core.Entities; |
| 12 | + using System.Collections.Generic; |
| 13 | + using System.Diagnostics; |
| 14 | + |
| 15 | + class EntitySqlBackendQueries : EntityBackendQueries |
| 16 | + { |
| 17 | + readonly SqlOrchestrationService orchestrationService; |
| 18 | + |
| 19 | + static TimeSpan timeLimitForCleanEntityStorageLoop = TimeSpan.FromSeconds(5); |
| 20 | + |
| 21 | + public EntitySqlBackendQueries( |
| 22 | + SqlOrchestrationService orchestrationService) |
| 23 | + { |
| 24 | + this.orchestrationService = orchestrationService; |
| 25 | + } |
| 26 | + |
| 27 | + public async override Task<EntityMetadata?> GetEntityAsync( |
| 28 | + EntityId id, |
| 29 | + bool includeState = false, |
| 30 | + bool includeStateless = false, |
| 31 | + CancellationToken cancellation = default) |
| 32 | + { |
| 33 | + OrchestrationState? state = (await this.orchestrationService.GetOrchestrationStateAsync(id.ToString(), allExecutions: false)).FirstOrDefault(); |
| 34 | + return this.GetEntityMetadata(state, includeStateless, includeState); |
| 35 | + } |
| 36 | + |
| 37 | + public async override Task<EntityQueryResult> QueryEntitiesAsync(EntityQuery filter, CancellationToken cancellation) |
| 38 | + { |
| 39 | + int pageNumber = 0; |
| 40 | + if (!string.IsNullOrEmpty(filter.ContinuationToken) && !int.TryParse(filter.ContinuationToken, out pageNumber)) |
| 41 | + { |
| 42 | + throw new ArgumentException($"Invalid continuation token {filter.ContinuationToken}"); |
| 43 | + } |
| 44 | + |
| 45 | + int retrievedResults = 0; |
| 46 | + IEnumerable<OrchestrationState> allResults = Array.Empty<OrchestrationState>(); |
| 47 | + var stopwatch = new Stopwatch(); |
| 48 | + stopwatch.Start(); |
| 49 | + do |
| 50 | + { |
| 51 | + SqlOrchestrationQuery entityInstancesQuery = new SqlOrchestrationQuery() |
| 52 | + { |
| 53 | + PageSize = filter.PageSize.GetValueOrDefault(100), |
| 54 | + PageNumber = pageNumber, |
| 55 | + InstanceIdPrefix = filter.InstanceIdStartsWith, |
| 56 | + CreatedTimeFrom = filter.LastModifiedFrom.GetValueOrDefault(DateTime.MinValue), |
| 57 | + CreatedTimeTo = filter.LastModifiedTo.GetValueOrDefault(DateTime.MaxValue), |
| 58 | + FetchInput = filter.IncludeState, |
| 59 | + }; |
| 60 | + IReadOnlyCollection<OrchestrationState> results = await this.orchestrationService.GetManyOrchestrationsAsync(entityInstancesQuery, cancellation); |
| 61 | + allResults = allResults.Concat(results); |
| 62 | + pageNumber++; |
| 63 | + |
| 64 | + retrievedResults = results.Count; |
| 65 | + if (retrievedResults == 0) |
| 66 | + { |
| 67 | + pageNumber = -1; |
| 68 | + } |
| 69 | + } while (retrievedResults > 0 && stopwatch.ElapsedMilliseconds <= 100); |
| 70 | + |
| 71 | + IEnumerable<EntityMetadata> entities = allResults.Select(result => this.GetEntityMetadata(result, filter.IncludeTransient, filter.IncludeState)) |
| 72 | + .OfType<EntityMetadata>(); |
| 73 | + |
| 74 | + return new EntityQueryResult() |
| 75 | + { |
| 76 | + Results = entities, |
| 77 | + ContinuationToken = pageNumber < 0 ? null : pageNumber.ToString() |
| 78 | + }; |
| 79 | + } |
| 80 | + |
| 81 | + public async override Task<CleanEntityStorageResult> CleanEntityStorageAsync(CleanEntityStorageRequest request = default, CancellationToken cancellation = default) |
| 82 | + { |
| 83 | + DateTime now = DateTime.UtcNow; |
| 84 | + int emptyEntitiesRemoved = 0; |
| 85 | + int orphanedLocksReleased = 0; |
| 86 | + int pageNumber = 0; |
| 87 | + if (!string.IsNullOrEmpty(request.ContinuationToken) && !int.TryParse(request.ContinuationToken, out pageNumber)) |
| 88 | + { |
| 89 | + throw new ArgumentException($"Invalid continuation token {request.ContinuationToken}"); |
| 90 | + } |
| 91 | + |
| 92 | + int retrievedResults = 0; |
| 93 | + IEnumerable<OrchestrationState> allResults = Array.Empty<OrchestrationState>(); |
| 94 | + var stopwatch = new Stopwatch(); |
| 95 | + stopwatch.Start(); |
| 96 | + do |
| 97 | + { |
| 98 | + SqlOrchestrationQuery entityInstancesQuery = new SqlOrchestrationQuery() |
| 99 | + { |
| 100 | + PageSize = 100, |
| 101 | + PageNumber = pageNumber, |
| 102 | + InstanceIdPrefix = "@", |
| 103 | + CreatedTimeFrom = DateTime.MinValue, |
| 104 | + CreatedTimeTo = DateTime.MaxValue, |
| 105 | + FetchInput = true, |
| 106 | + }; |
| 107 | + |
| 108 | + IReadOnlyCollection<OrchestrationState> page = await this.orchestrationService.GetManyOrchestrationsAsync(entityInstancesQuery, cancellation); |
| 109 | + |
| 110 | + pageNumber++; |
| 111 | + retrievedResults = page.Count; |
| 112 | + if (retrievedResults == 0) |
| 113 | + { |
| 114 | + pageNumber = -1; |
| 115 | + break; |
| 116 | + } |
| 117 | + |
| 118 | + var tasks = new List<Task>(); |
| 119 | + IEnumerable<string> emptyEntityIds = new List<string>(); |
| 120 | + |
| 121 | + foreach (OrchestrationState state in page) |
| 122 | + { |
| 123 | + EntityStatus? status = ClientEntityHelpers.GetEntityStatus(state.Status); |
| 124 | + if (status != null) |
| 125 | + { |
| 126 | + if (request.ReleaseOrphanedLocks && status.LockedBy != null) |
| 127 | + { |
| 128 | + tasks.Add(CheckForOrphanedLockAndFixIt(state, status.LockedBy)); |
| 129 | + } |
| 130 | + |
| 131 | + if (request.RemoveEmptyEntities) |
| 132 | + { |
| 133 | + bool isEmptyEntity = !status.EntityExists && status.LockedBy == null && status.BacklogQueueSize == 0; |
| 134 | + bool safeToRemoveWithoutBreakingMessageSorterLogic = |
| 135 | + now - state.LastUpdatedTime > this.orchestrationService.EntityBackendProperties.EntityMessageReorderWindow; |
| 136 | + if (isEmptyEntity && safeToRemoveWithoutBreakingMessageSorterLogic) |
| 137 | + { |
| 138 | + emptyEntityIds.Append(state.OrchestrationInstance.InstanceId); |
| 139 | + orphanedLocksReleased++; |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + async Task CheckForOrphanedLockAndFixIt(OrchestrationState state, string lockOwner) |
| 146 | + { |
| 147 | + OrchestrationState? ownerState |
| 148 | + = (await this.orchestrationService.GetOrchestrationStateAsync(lockOwner, allExecutions: false)).FirstOrDefault(); |
| 149 | + |
| 150 | + bool OrchestrationIsRunning(OrchestrationStatus? status) |
| 151 | + => status != null && (status == OrchestrationStatus.Running || status == OrchestrationStatus.Suspended); |
| 152 | + |
| 153 | + if (!OrchestrationIsRunning(ownerState?.OrchestrationStatus)) |
| 154 | + { |
| 155 | + // the owner is not a running orchestration. Send a lock release. |
| 156 | + EntityMessageEvent eventToSend = ClientEntityHelpers.EmitUnlockForOrphanedLock(state.OrchestrationInstance, lockOwner); |
| 157 | + await this.orchestrationService.SendTaskOrchestrationMessageAsync(eventToSend.AsTaskMessage()); |
| 158 | + Interlocked.Increment(ref orphanedLocksReleased); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + await this.orchestrationService.PurgeOrchestrationHistoryAsync(emptyEntityIds); |
| 163 | + |
| 164 | + } while (retrievedResults > 0 && stopwatch.Elapsed <= timeLimitForCleanEntityStorageLoop); |
| 165 | + |
| 166 | + return new CleanEntityStorageResult() |
| 167 | + { |
| 168 | + EmptyEntitiesRemoved = emptyEntitiesRemoved, |
| 169 | + OrphanedLocksReleased = orphanedLocksReleased, |
| 170 | + ContinuationToken = pageNumber < 0 ? null : pageNumber.ToString() |
| 171 | + }; |
| 172 | + } |
| 173 | + |
| 174 | + EntityMetadata? GetEntityMetadata(OrchestrationState? state, bool includeTransient, bool includeState) |
| 175 | + { |
| 176 | + if (state == null) |
| 177 | + { |
| 178 | + return null; |
| 179 | + } |
| 180 | + |
| 181 | + if (!includeState) |
| 182 | + { |
| 183 | + if (!includeTransient) |
| 184 | + { |
| 185 | + // it is possible that this entity was logically deleted even though its orchestration was not purged yet. |
| 186 | + // we can check this efficiently (i.e. without deserializing anything) by looking at just the custom status |
| 187 | + if (!EntityStatus.TestEntityExists(state.Status)) |
| 188 | + { |
| 189 | + return null; |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + EntityStatus? status = ClientEntityHelpers.GetEntityStatus(state.Status); |
| 194 | + |
| 195 | + return new EntityMetadata() |
| 196 | + { |
| 197 | + EntityId = EntityId.FromString(state.OrchestrationInstance.InstanceId), |
| 198 | + LastModifiedTime = state.CreatedTime, |
| 199 | + BacklogQueueSize = status?.BacklogQueueSize ?? 0, |
| 200 | + LockedBy = status?.LockedBy, |
| 201 | + SerializedState = null, // we were instructed to not include the state |
| 202 | + }; |
| 203 | + } |
| 204 | + else |
| 205 | + { |
| 206 | + // return the result to the user |
| 207 | + if (!includeTransient && state.Input == null) |
| 208 | + { |
| 209 | + return null; |
| 210 | + } |
| 211 | + else |
| 212 | + { |
| 213 | + EntityStatus? status = ClientEntityHelpers.GetEntityStatus(state.Status); |
| 214 | + |
| 215 | + return new EntityMetadata() |
| 216 | + { |
| 217 | + EntityId = EntityId.FromString(state.OrchestrationInstance.InstanceId), |
| 218 | + LastModifiedTime = state.CreatedTime, |
| 219 | + BacklogQueueSize = status?.BacklogQueueSize ?? 0, |
| 220 | + LockedBy = status?.LockedBy, |
| 221 | + SerializedState = state.Input, |
| 222 | + }; |
| 223 | + } |
| 224 | + } |
| 225 | + } |
| 226 | + } |
| 227 | +} |
0 commit comments