From a7f4538ffa85520f2281bd9fcb7da2ddb8b57739 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Tue, 21 Nov 2023 14:21:25 +0100
Subject: [PATCH] Use exponential backoff when polling running executions

#85
---
 src/utils/BackoffStrategy.ts                  | 70 +++++++++++++++++++
 .../workflows/ListWorkflowExecutionsView.vue  | 44 +++++++++---
 2 files changed, 103 insertions(+), 11 deletions(-)
 create mode 100644 src/utils/BackoffStrategy.ts

diff --git a/src/utils/BackoffStrategy.ts b/src/utils/BackoffStrategy.ts
new file mode 100644
index 0000000..c10ad0a
--- /dev/null
+++ b/src/utils/BackoffStrategy.ts
@@ -0,0 +1,70 @@
+abstract class BackoffStrategy {
+  protected currentVal: number;
+  protected iteration: number;
+  protected reachedMax: boolean;
+  protected maxValue: number;
+
+  constructor(maxValue?: number) {
+    this.currentVal = 0;
+    this.iteration = 0;
+    this.reachedMax = false;
+    this.maxValue = maxValue ?? 300;
+  }
+
+  protected abstract computeNextValue(): number;
+
+  public reset() {
+    this.iteration = 0;
+    this.currentVal = 0;
+    this.reachedMax = false;
+  }
+
+  public *generator(): Generator<number> {
+    while (true) {
+      this.iteration++;
+      if (this.reachedMax) {
+        yield this.maxValue;
+      } else {
+        this.currentVal = this.computeNextValue();
+        if (0 < this.maxValue && this.maxValue < this.currentVal) {
+          this.reachedMax = true;
+          yield this.maxValue;
+        } else {
+          yield this.currentVal;
+        }
+      }
+    }
+  }
+}
+
+export class ExponentialBackoff extends BackoffStrategy {
+  protected computeNextValue(): number {
+    return 2 << (this.iteration - 1);
+  }
+}
+
+export class NoBackoff extends BackoffStrategy {
+  private readonly constantValue: number;
+
+  constructor(constantValue?: number) {
+    super();
+    this.constantValue = constantValue ?? 30;
+  }
+
+  protected computeNextValue(): number {
+    return this.constantValue;
+  }
+}
+
+export class LinearBackoff extends BackoffStrategy {
+  private readonly backoff: number;
+
+  constructor(backoff?: number) {
+    super();
+    this.backoff = backoff ?? 5;
+  }
+
+  protected computeNextValue(): number {
+    return this.currentVal + this.backoff;
+  }
+}
diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue
index ea395ae..cbf7b1f 100644
--- a/src/views/workflows/ListWorkflowExecutionsView.vue
+++ b/src/views/workflows/ListWorkflowExecutionsView.vue
@@ -8,12 +8,14 @@ import DeleteModal from "@/components/modals/DeleteModal.vue";
 import { useWorkflowStore } from "@/stores/workflows";
 import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
 import ParameterModal from "@/components/workflows/modals/ParameterModal.vue";
+import { ExponentialBackoff } from "@/utils/BackoffStrategy";
 
 const workflowRepository = useWorkflowStore();
 const executionRepository = useWorkflowExecutionStore();
+const backoff = new ExponentialBackoff();
 
 let refreshTimeout: NodeJS.Timeout | undefined = undefined;
-let intervalId: NodeJS.Timer | undefined = undefined;
+let pollingTimeout: NodeJS.Timeout | undefined = undefined;
 
 const executionsState = reactive<{
   loading: boolean;
@@ -80,9 +82,17 @@ const deleteModalString = computed<string>(() => {
 // Functions
 // -----------------------------------------------------------------------------
 function updateExecutions() {
-  executionRepository.fetchExecutions(() => {
-    executionsState.loading = false;
-  });
+  backoff.reset();
+  clearTimeout(pollingTimeout);
+  executionRepository
+    .fetchExecutions(() => {
+      executionsState.loading = false;
+    })
+    .then(() => {
+      if (runningExecutions.value.length > 0) {
+        refreshRunningWorkflowExecutionTimer();
+      }
+    });
 }
 
 function refreshExecutions() {
@@ -106,25 +116,37 @@ function cancelWorkflowExecution(executionId: string) {
   executionRepository.cancelExecution(executionId);
 }
 
+const runningExecutions = computed<WorkflowExecutionOut[]>(() =>
+  executionRepository.executions.filter((execution) =>
+    workflowExecutionCancelable(execution),
+  ),
+);
+
 function refreshRunningWorkflowExecution() {
   Promise.all(
-    executionRepository.executions
-      .filter((execution) => workflowExecutionCancelable(execution))
-      .map((execution) =>
-        executionRepository.fetchExecution(execution.execution_id),
-      ),
+    runningExecutions.value.map((execution) =>
+      executionRepository.fetchExecution(execution.execution_id),
+    ),
   );
 }
 
+async function refreshRunningWorkflowExecutionTimer() {
+  for (const sleep of backoff.generator()) {
+    await new Promise((resolve) => {
+      pollingTimeout = setTimeout(resolve, sleep * 1000);
+    });
+    refreshRunningWorkflowExecution();
+  }
+}
+
 onMounted(() => {
   updateExecutions();
   workflowRepository.fetchWorkflows();
-  intervalId = setInterval(refreshRunningWorkflowExecution, 5000);
   new Tooltip("#refreshExecutionsButton");
 });
 
 onUnmounted(() => {
-  clearInterval(intervalId);
+  clearTimeout(pollingTimeout);
 });
 </script>
 
-- 
GitLab