Skip to content
This repository has been archived by the owner on Mar 2, 2022. It is now read-only.

Commit

Permalink
batch purge of Durable Entities
Browse files Browse the repository at this point in the history
  • Loading branch information
scale-tone committed Feb 5, 2020
1 parent bb6882a commit 5b9874d
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum EntityTypeEnum
// Adds extra fields to original DurableOrchestrationStatus
public class ExpandedOrchestrationStatus : DurableOrchestrationStatus
{
public static readonly Regex EntityIdRegex = new Regex(@"@(\w+)@(.+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public EntityTypeEnum EntityType { get; private set; }
public EntityId? EntityId { get; private set; }

Expand Down Expand Up @@ -78,7 +79,7 @@ public ExpandedOrchestrationStatus(DurableOrchestrationStatus that, Task<Durable
this.History = that.History;

// Detecting whether it is an Orchestration or a Durable Entity
var match = Orchestration.EntityIdRegex.Match(this.InstanceId);
var match = EntityIdRegex.Match(this.InstanceId);
if(match.Success)
{
this.EntityType = EntityTypeEnum.DurableEntity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ namespace DurableFunctionsMonitor.DotNetBackend
// POST /api/orchestrations('<id>')/raise-event
public static class Orchestration
{
public static readonly Regex EntityIdRegex = new Regex(@"@(\w+)@(.+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

[FunctionName("orchestration")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "orchestrations('{instanceId}')/{action?}")] HttpRequest req,
Expand Down
43 changes: 41 additions & 2 deletions durablefunctionsmonitor.dotnetbackend/Functions/PurgeHistory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
Expand All @@ -18,6 +19,7 @@ class PurgeHistoryRequest
public string TimeFrom { get; set; }
public string TimeTill { get; set; }
public OrchestrationStatus[] Statuses { get; set; }
public EntityTypeEnum EntityType { get; set; }
}

// Purges orchestration instance history
Expand All @@ -40,10 +42,47 @@ class PurgeHistoryRequest
// Important to deserialize time fields as strings, because otherwize time zone will appear to be local
var request = JsonConvert.DeserializeObject<PurgeHistoryRequest>(await req.ReadAsStringAsync());

var result = await durableClient.PurgeInstanceHistoryAsync(DateTime.Parse(request.TimeFrom),
DateTime.Parse(request.TimeTill), request.Statuses);
var result = request.EntityType == EntityTypeEnum.DurableEntity ?
await durableClient.PurgeDurableEntitiesHistory(DateTime.Parse(request.TimeFrom),
DateTime.Parse(request.TimeTill)) :
await durableClient.PurgeOrchestrationsHistory(DateTime.Parse(request.TimeFrom),
DateTime.Parse(request.TimeTill), request.Statuses);

return new JsonResult(result, Globals.SerializerSettings);
}

private static Task<PurgeHistoryResult> PurgeOrchestrationsHistory(
this IDurableClient durableClient,
DateTime timeFrom,
DateTime timeTill,
OrchestrationStatus[] statuses)
{
return durableClient.PurgeInstanceHistoryAsync(timeFrom, timeTill, statuses);
}

private static async Task<PurgeHistoryResult> PurgeDurableEntitiesHistory(
this IDurableClient durableClient,
DateTime timeFrom,
DateTime timeTill)
{

// The only known way of retrieving Durable Entities _only_ is to ask for running instances
// (because Durable Entities are always "Running") and then check their InstanceIds for two at signs in them.
var entities = (await durableClient.GetStatusAsync(
timeFrom, timeTill,
new OrchestrationRuntimeStatus[] { OrchestrationRuntimeStatus.Running }
))
.Where(en => ExpandedOrchestrationStatus.EntityIdRegex.Match(en.InstanceId).Success);

int instancesDeleted = 0;

foreach(var entity in entities)
{
await durableClient.PurgeInstanceHistoryAsync(entity.InstanceId);
instancesDeleted++;
}

return new PurgeHistoryResult(instancesDeleted);
}
}
}
2 changes: 1 addition & 1 deletion durablefunctionsmonitor.dotnetbackend/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link rel="manifest" href="/api/monitor/manifest.json"><link rel="shortcut icon" href="/api/monitor/favicon.png"><title>Durable Functions Monitor</title><link href="/api/monitor/static/css/2.c6250e3a.chunk.css" rel="stylesheet"><link href="/api/monitor/static/css/main.8d90dfff.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><script>var OrchestrationIdFromVsCode=""</script><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],i=0,a=[];i<n.length;i++)t=n[i],Object.prototype.hasOwnProperty.call(c,t)&&c[t]&&a.push(c[t][0]),c[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);a.length;)a.shift()();return p.push.apply(p,u||[]),f()}function f(){for(var e,r=0;r<p.length;r++){for(var t=p[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==c[u]&&(n=!1)}n&&(p.splice(r--,1),e=i(i.s=t[0]))}return e}var t={},c={1:0},p=[];function i(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=l,i.c=t,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(r,e){if(1&e&&(r=i(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)i.d(t,n,function(e){return r[e]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/api/monitor/";var r=this["webpackJsonpdurablefunctionsmonitor.react"]=this["webpackJsonpdurablefunctionsmonitor.react"]||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;f()}([])</script><script src="/api/monitor/static/js/2.022eef16.chunk.js"></script><script src="/api/monitor/static/js/main.e4b641d1.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link rel="manifest" href="/api/monitor/manifest.json"><link rel="shortcut icon" href="/api/monitor/favicon.png"><title>Durable Functions Monitor</title><link href="/api/monitor/static/css/2.c6250e3a.chunk.css" rel="stylesheet"><link href="/api/monitor/static/css/main.8d90dfff.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><script>var OrchestrationIdFromVsCode=""</script><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],i=0,a=[];i<n.length;i++)t=n[i],Object.prototype.hasOwnProperty.call(c,t)&&c[t]&&a.push(c[t][0]),c[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);a.length;)a.shift()();return p.push.apply(p,u||[]),f()}function f(){for(var e,r=0;r<p.length;r++){for(var t=p[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==c[u]&&(n=!1)}n&&(p.splice(r--,1),e=i(i.s=t[0]))}return e}var t={},c={1:0},p=[];function i(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=l,i.c=t,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(r,e){if(1&e&&(r=i(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)i.d(t,n,function(e){return r[e]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/api/monitor/";var r=this["webpackJsonpdurablefunctionsmonitor.react"]=this["webpackJsonpdurablefunctionsmonitor.react"]||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;f()}([])</script><script src="/api/monitor/static/js/2.022eef16.chunk.js"></script><script src="/api/monitor/static/js/main.a0ea1d29.chunk.js"></script></body></html>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

importScripts(
"/api/monitor/precache-manifest.bd6fa2fa783beb1ab05d493c125a1784.js"
"/api/monitor/precache-manifest.6d3c04e65869ef9f242f8ee3b831204e.js"
);

self.addEventListener('message', (event) => {
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion durablefunctionsmonitor.react/src/components/MainMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class MainMenu extends React.Component<{ state: MainMenuState }> {
onClose={() => state.menuAnchorElement = undefined}
>
<MenuItem onClick={() => state.showConnectionParamsDialog()}>Manage Storage Connection Settings...</MenuItem>
<MenuItem onClick={() => state.showPurgeHistoryDialog()}>Purge Orchestration Instance History...</MenuItem>
<MenuItem onClick={() => state.showPurgeHistoryDialog()}>Purge Instance History...</MenuItem>
</Menu>

<Dialog
Expand Down
68 changes: 55 additions & 13 deletions durablefunctionsmonitor.react/src/components/PurgeHistoryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { observer } from 'mobx-react';

import {
Box, Checkbox, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl,
FormControlLabel, FormGroup, FormLabel, LinearProgress, TextField
FormControlLabel, FormGroup, FormLabel, LinearProgress, Radio, RadioGroup, TextField, Tooltip, Typography
} from '@material-ui/core';

import './PurgeHistoryDialog.css';

import { DateTimeHelpers } from '../DateTimeHelpers';
import { ErrorMessage } from './ErrorMessage';
import { RuntimeStatus } from '../states/DurableOrchestrationStatus';
import { EntityType, RuntimeStatus } from '../states/DurableOrchestrationStatus';
import { PurgeHistoryDialogState } from '../states/PurgeHistoryDialogState';

// Dialog with parameters for purging orchestration instance history
Expand All @@ -23,7 +23,7 @@ export class PurgeHistoryDialog extends React.Component<{ state: PurgeHistoryDia
return (
<Dialog open={state.dialogOpen} onClose={() => { if (!state.inProgress) state.dialogOpen = false; }}>

<DialogTitle>Purge Orchestration Instance History</DialogTitle>
<DialogTitle>Purge Instance History</DialogTitle>

{state.instancesDeleted === null && (
<div>
Expand All @@ -32,9 +32,38 @@ export class PurgeHistoryDialog extends React.Component<{ state: PurgeHistoryDia
{state.inProgress ? (<LinearProgress />) : (<Box height={4} />)}

<DialogContentText>
WARNING: this operation drops orchestration states from the underlying storage and cannot be undone.
WARNING: this operation drops instance states from the underlying storage and cannot be undone.

{state.entityType === "DurableEntity" && (
<Typography color="error" >
It might as well remove Durable Entities, that are still active.
Ensure that you specify the correct time frame!
</Typography>
)}

</DialogContentText>

<FormControl className="purge-history-statuses" disabled={state.inProgress} fullWidth>
<FormLabel>Apply to:</FormLabel>
<RadioGroup row
value={state.entityType}
onChange={(evt) => state.entityType = (evt.target as HTMLInputElement).value as EntityType}
>
<FormControlLabel
disabled={state.inProgress}
value={"Orchestration"}
control={<Radio />}
label="Orchestrations"
/>
<FormControlLabel
disabled={state.inProgress}
value={"DurableEntity"}
control={<Radio />}
label="Durable Entities"
/>
</RadioGroup>
</FormControl>

<TextField
className="purge-history-from-input"
label="From (UTC)"
Expand All @@ -56,15 +85,28 @@ export class PurgeHistoryDialog extends React.Component<{ state: PurgeHistoryDia
/>

<FormControl className="purge-history-statuses" disabled={state.inProgress}>
<FormLabel>Remove orchestrations with the following status:</FormLabel>
<FormGroup row>

<RuntimeStatusCheckbox state={state} runtimeStatus="Completed" />
<RuntimeStatusCheckbox state={state} runtimeStatus="Failed" />
<RuntimeStatusCheckbox state={state} runtimeStatus="Terminated" />

</FormGroup>
<FormLabel>(Only these three are supported by the API, sorry)</FormLabel>
<FormLabel>With the following status:</FormLabel>

{state.entityType === "Orchestration" && (
<FormGroup row>
<RuntimeStatusCheckbox state={state} runtimeStatus="Completed" />
<RuntimeStatusCheckbox state={state} runtimeStatus="Failed" />
<RuntimeStatusCheckbox state={state} runtimeStatus="Terminated" />
</FormGroup>
)}

{state.entityType === "DurableEntity" && (
<FormGroup row>
<Tooltip title="Durable Entities are always in 'Running' state">
<FormControlLabel
control={<Checkbox
checked={true} />}
label="Running"
disabled={true}
/>
</Tooltip>
</FormGroup>
)}
</FormControl>

<ErrorMessage state={state} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { observable, computed } from 'mobx'

import { IBackendClient } from '../services/IBackendClient';
import { RuntimeStatus } from './DurableOrchestrationStatus';
import { RuntimeStatus, EntityType } from './DurableOrchestrationStatus';
import { ErrorMessageState } from './ErrorMessageState';


// State of Purge History Dialog
export class PurgeHistoryDialogState extends ErrorMessageState {

Expand All @@ -24,6 +23,8 @@ export class PurgeHistoryDialogState extends ErrorMessageState {
this.timeTill = new Date();

this._statuses = new Set<RuntimeStatus>(["Completed", "Terminated"]);

this.entityType = "Orchestration";
}
}

Expand All @@ -45,6 +46,7 @@ export class PurgeHistoryDialogState extends ErrorMessageState {
this._inProgress = true;

this._backendClient.call('POST', '/purge-history', {
entityType: this.entityType,
timeFrom: this.timeFrom,
timeTill: this.timeTill,
statuses: Array.from(this._statuses.values())
Expand All @@ -64,6 +66,9 @@ export class PurgeHistoryDialogState extends ErrorMessageState {
@observable
timeTill: Date = new Date();

@observable
entityType: EntityType = "Orchestration";

getStatusIncluded(status: RuntimeStatus) {
return this._statuses.has(status);
}
Expand Down

0 comments on commit 5b9874d

Please sign in to comment.