Merge pull request #17355 from davelopez/unify_continuous_polling

Unify continuous polling with composable
This commit is contained in:
David López
2024-01-30 11:51:10 +01:00
committed by GitHub
13 changed files with 148 additions and 107 deletions
@@ -9,7 +9,7 @@ import SelectedItems from "@/components/History/Content/SelectedItems";
import { HistoryFilters } from "@/components/History/HistoryFilters";
import { deleteContent, updateContentFields } from "@/components/History/model/queries";
import { Toast } from "@/composables/toast";
import { rewatchHistory } from "@/store/historyStore/model/watchHistory";
import { startWatchingHistory } from "@/store/historyStore/model/watchHistory";
import { type HistoryItem, useHistoryItemsStore } from "@/stores/historyItemsStore";
import { useHistoryStore } from "@/stores/historyStore";
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
@@ -277,7 +277,7 @@ async function onUnhide(item: HistoryItem) {
}
function reloadContents() {
rewatchHistory();
startWatchingHistory();
}
function setInvisible(item: HistoryItem) {
@@ -4,7 +4,7 @@
*/
import Backbone from "backbone";
import { createDatasetCollection } from "components/History/model/queries";
import { watchHistory } from "store/historyStore/model/watchHistory";
import { startWatchingHistory } from "store/historyStore/model/watchHistory";
import { useHistoryItemsStore } from "stores/historyItemsStore";
import { useHistoryStore } from "stores/historyStore";
@@ -26,7 +26,7 @@ export class HistoryPanelProxy {
};
// start watching the history with continuous queries
watchHistory();
startWatchingHistory();
}
syncCurrentHistoryModel(currentHistory) {
@@ -134,9 +134,8 @@ export default {
this.load();
},
methods: {
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints", "removeEntryPoint"]),
...mapActions(useEntryPointStore, ["removeEntryPoint"]),
load() {
this.ensurePollingEntryPoints();
this.filter = "";
},
filtered: function (items) {
+1 -1
View File
@@ -89,7 +89,7 @@ watch(
/* lifecyle */
onBeforeMount(() => {
entryPointStore = useEntryPointStore();
entryPointStore.ensurePollingEntryPoints();
entryPointStore.startWatchingEntryPoints();
entryPointStore.$subscribe((mutation, state) => {
updateVisibility(state.entryPoints.length > 0);
});
@@ -41,7 +41,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { getAppRoot } from "onload/loadConfig";
import { mapActions, mapState } from "pinia";
import { mapState } from "pinia";
import { useEntryPointStore } from "stores/entryPointStore";
library.add(faExternalLinkAlt);
@@ -62,11 +62,5 @@ export default {
return getAppRoot() + "interactivetool_entry_points/list";
},
},
created: function () {
this.ensurePollingEntryPoints();
},
methods: {
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints"]),
},
};
</script>
+94
View File
@@ -0,0 +1,94 @@
export type WatchResourceHandler = () => Promise<void>;
export interface WatchOptions {
/**
* Polling interval in milliseconds when the app is active (in the current tab).
*/
shortPollingInterval?: number;
/**
* Polling interval in milliseconds when the app is in the background (not in the current tab).
*/
longPollingInterval?: number;
/**
* If true, the resource is watched in the background even when the app is not active (in the current tab).
*/
enableBackgroundPolling?: boolean;
}
const DEFAULT_WATCH_OPTIONS: WatchOptions = {
shortPollingInterval: 3000,
longPollingInterval: 10000,
enableBackgroundPolling: true,
};
/**
* Creates a composable that watches a resource by polling the server continuously.
* By default, the polling interval is 'short' when the app is active (in the current tab) and 'long'
* when the app is in the background (not in the current tab).
* You can also completely disable background polling by setting `enableBackgroundPolling` to false in the options.
* @param watchHandler The handler function that watches the resource by querying the server.
* @param options Options to customize the polling interval.
*/
export function useResourceWatcher(watchHandler: WatchResourceHandler, options: WatchOptions = DEFAULT_WATCH_OPTIONS) {
const { shortPollingInterval, longPollingInterval, enableBackgroundPolling } = {
...DEFAULT_WATCH_OPTIONS,
...options,
};
let currentPollingInterval = shortPollingInterval;
let watchTimeout: NodeJS.Timeout | null = null;
let isEventSetup = false;
/**
* Starts watching the resource by polling the server continuously.
*/
function startWatchingResource() {
stopWatchingResource();
tryWatchResource();
}
/**
* Stops continuously watching the resource.
*/
function stopWatchingResource() {
if (watchTimeout) {
clearTimeout(watchTimeout);
watchTimeout = null;
}
}
async function tryWatchResource() {
try {
await watchHandler();
} catch (error) {
console.warn(error);
} finally {
if (currentPollingInterval) {
watchTimeout = setTimeout(() => {
tryWatchResource();
}, currentPollingInterval);
}
}
}
function setupVisibilityListeners() {
if (!isEventSetup) {
isEventSetup = true;
document.addEventListener("visibilitychange", updateThrottle);
}
}
function updateThrottle() {
if (document.visibilityState === "visible") {
currentPollingInterval = shortPollingInterval;
startWatchingResource();
} else {
currentPollingInterval = enableBackgroundPolling ? longPollingInterval : undefined;
}
}
setupVisibilityListeners();
return {
startWatchingResource,
};
}
+3 -3
View File
@@ -173,7 +173,7 @@ export default {
this.Galaxy.modal = new Modal.View();
this.Galaxy.frame = this.windowManager;
if (this.Galaxy.config.enable_notification_system) {
this.startNotificationsPolling();
this.startWatchingNotifications();
}
}
},
@@ -189,9 +189,9 @@ export default {
}
},
methods: {
startNotificationsPolling() {
startWatchingNotifications() {
const notificationsStore = useNotificationsStore();
notificationsStore.startPollingNotifications();
notificationsStore.startWatchingNotifications();
},
openUrl(urlObj) {
if (!urlObj.target) {
@@ -13,13 +13,14 @@ import { getCurrentHistoryFromServer } from "stores/services/history.services";
import { loadSet } from "utils/setCache";
import { urlData } from "utils/url";
import { useResourceWatcher } from "@/composables/resourceWatcher";
import { useCollectionElementsStore } from "@/stores/collectionElementsStore";
import { useDatasetStore } from "@/stores/datasetStore";
const limit = 1000;
let throttlePeriod = 3000;
let watchTimeout = null;
const ACTIVE_POLLING_INTERVAL = 3000;
const INACTIVE_POLLING_INTERVAL = 60000;
// last time the history has changed
let lastUpdateTime = null;
@@ -27,17 +28,22 @@ let lastUpdateTime = null;
// last time changed history items have been requested
let lastRequestDate = new Date();
// We only want to kick this off once we're actively watching history
let watchingVisibility = false;
const { startWatchingResource: startWatchingHistory } = useResourceWatcher(watchHistory, {
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
longPollingInterval: INACTIVE_POLLING_INTERVAL,
});
function setVisibilityThrottle() {
if (document.visibilityState === "visible") {
// Poll every 3 seconds when visible
throttlePeriod = 3000;
rewatchHistory();
} else {
// Poll every 60 seconds when hidden/backgrounded
throttlePeriod = 60000;
export { startWatchingHistory };
async function watchHistory() {
const { isWatching } = storeToRefs(useHistoryItemsStore());
try {
isWatching.value = true;
await watchHistoryOnce();
} catch (error) {
// error alerting the user that watch history failed
console.warn(error);
isWatching.value = false;
}
}
@@ -46,8 +52,7 @@ export async function watchHistoryOnce() {
const historyItemsStore = useHistoryItemsStore();
const datasetStore = useDatasetStore();
const collectionElementsStore = useCollectionElementsStore();
// "Reset" watchTimeout so we don't queue up watchHistory calls in rewatchHistory.
watchTimeout = null;
// get current history
const checkForUpdate = new Date();
const history = await getCurrentHistoryFromServer(lastUpdateTime);
@@ -96,35 +101,6 @@ export async function watchHistoryOnce() {
}
}
export async function watchHistory() {
const { isWatching } = storeToRefs(useHistoryItemsStore());
// Only set up visibility listeners once, whenever a watch is first started
if (watchingVisibility === false) {
watchingVisibility = true;
isWatching.value = watchingVisibility;
document.addEventListener("visibilitychange", setVisibilityThrottle);
}
try {
await watchHistoryOnce();
} catch (error) {
// error alerting the user that watch history failed
console.warn(error);
watchingVisibility = false;
isWatching.value = watchingVisibility;
} finally {
watchTimeout = setTimeout(() => {
watchHistory();
}, throttlePeriod);
}
}
export function rewatchHistory() {
if (watchTimeout) {
clearTimeout(watchTimeout);
watchHistory();
}
}
/**
* Returns the set of history item IDs that are currently expanded in the history panel from the cache.
* These content items need to retrieve detailed information when updated.
@@ -70,7 +70,7 @@ describe("watchHistory", () => {
.replyOnce(200, historyData)
.onGet(/api\/histories\/history-id\/contents?.*/)
.replyOnce(200, historyItems);
await watchHistoryOnce(wrapper.vm.$store);
await watchHistoryOnce();
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
expect(wrapper.vm.getHistoryItems("history-id", "second")[0].hid).toBe(2);
expect(wrapper.vm.getHistoryItems("history-id", "state:ok")[0].hid).toBe(1);
@@ -86,11 +86,11 @@ describe("watchHistory", () => {
.onGet(`/history/current_history_json`)
.replyOnce(500);
await watchHistoryOnce(wrapper.vm.$store);
await watchHistoryOnce();
expect(wrapper.vm.currentHistoryId).toBe("history-id");
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
try {
await watchHistoryOnce(wrapper.vm.$store);
await watchHistoryOnce();
} catch (error) {
console.log(error);
expect(error.response.status).toBe(500);
@@ -113,7 +113,7 @@ describe("watchHistory", () => {
history_id: "history-id",
},
]);
await watchHistoryOnce(wrapper.vm.$store);
await watchHistoryOnce();
// We should have received the update and have 3 items in the history
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(3);
});
+1 -10
View File
@@ -15,7 +15,7 @@ describe("stores/EntryPointStore", () => {
setActivePinia(createPinia());
axiosMock.onGet("/api/entry_points", { params: { running: true } }).reply(200, testInteractiveToolsResponse);
store = useEntryPointStore();
store.ensurePollingEntryPoints();
await store.fetchEntryPoints();
await flushPromises();
});
@@ -23,16 +23,7 @@ describe("stores/EntryPointStore", () => {
axiosMock.restore();
});
it("polls", async () => {
expect(store.entryPoints.length).toBe(2);
});
it("stops polling", async () => {
expect(store.pollTimeout !== undefined).toBeTruthy();
store.stopPollingEntryPoints();
expect(store.pollTimeout === undefined).toBeTruthy();
});
it("performs a partial update", async () => {
store.stopPollingEntryPoints();
const updateData = [
{
model_class: "InteractiveToolEntryPoint",
+9 -16
View File
@@ -3,9 +3,12 @@ import isEqual from "lodash.isequal";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useResourceWatcher } from "@/composables/resourceWatcher";
import { getAppRoot } from "@/onload/loadConfig";
import { rethrowSimple } from "@/utils/simple-error";
const ACTIVE_POLLING_INTERVAL = 10000;
// TODO: replace with the corresponding autogenerated model when ready
interface EntryPoint {
model_class: "InteractiveToolEntryPoint";
@@ -20,7 +23,11 @@ interface EntryPoint {
}
export const useEntryPointStore = defineStore("entryPointStore", () => {
const pollTimeout = ref<NodeJS.Timeout | undefined>(undefined);
const { startWatchingResource: startWatchingEntryPoints } = useResourceWatcher(fetchEntryPoints, {
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
enableBackgroundPolling: false, // No need to poll in the background
});
const entryPoints = ref<EntryPoint[]>([]);
const entryPointsForJob = computed(() => {
@@ -32,18 +39,6 @@ export const useEntryPointStore = defineStore("entryPointStore", () => {
entryPoints.value.filter((entryPoint) => entryPoint["output_datasets_ids"].includes(hdaId));
});
async function ensurePollingEntryPoints() {
await fetchEntryPoints();
pollTimeout.value = setTimeout(() => {
ensurePollingEntryPoints();
}, 10000);
}
function stopPollingEntryPoints() {
clearTimeout(pollTimeout.value);
pollTimeout.value = undefined;
}
async function fetchEntryPoints() {
const url = `${getAppRoot()}api/entry_points`;
const params = { running: true };
@@ -96,8 +91,6 @@ export const useEntryPointStore = defineStore("entryPointStore", () => {
fetchEntryPoints,
updateEntryPoints,
removeEntryPoint,
pollTimeout,
ensurePollingEntryPoints,
stopPollingEntryPoints,
startWatchingEntryPoints,
};
});
+9 -15
View File
@@ -7,19 +7,24 @@ import {
loadNotificationsStatus,
updateBatchNotificationsOnServer,
} from "@/api/notifications";
import { useResourceWatcher } from "@/composables/resourceWatcher";
import { mergeObjectListsById } from "@/utils/utils";
import { useBroadcastsStore } from "./broadcastsStore";
const STATUS_POLLING_DELAY = 5000;
const ACTIVE_POLLING_INTERVAL = 5000;
const INACTIVE_POLLING_INTERVAL = 30000;
export const useNotificationsStore = defineStore("notificationsStore", () => {
const { startWatchingResource: startWatchingNotifications } = useResourceWatcher(getNotificationStatus, {
shortPollingInterval: ACTIVE_POLLING_INTERVAL,
longPollingInterval: INACTIVE_POLLING_INTERVAL,
});
const broadcastsStore = useBroadcastsStore();
const totalUnreadCount = ref<number>(0);
const notifications = ref<UserNotification[]>([]);
const pollId = ref<NodeJS.Timeout | undefined>(undefined);
const loadingNotifications = ref<boolean>(false);
const lastNotificationUpdate = ref<Date | null>(null);
@@ -31,7 +36,6 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
}
async function getNotificationStatus() {
stopPollingNotifications();
try {
if (!lastNotificationUpdate.value) {
loadingNotifications.value = true;
@@ -56,22 +60,12 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
}
}
async function startPollingNotifications() {
await getNotificationStatus();
pollId.value = setTimeout(() => startPollingNotifications(), STATUS_POLLING_DELAY);
}
function stopPollingNotifications() {
clearTimeout(pollId.value);
pollId.value = undefined;
}
async function updateBatchNotification(request: UserNotificationsBatchUpdateRequest) {
await updateBatchNotificationsOnServer(request);
if (request.changes.deleted) {
notifications.value = notifications.value.filter((n) => !request.notification_ids.includes(n.id));
}
await startPollingNotifications();
startWatchingNotifications();
}
async function updateNotification(notification: UserNotification, changes: NotificationChanges) {
@@ -85,6 +79,6 @@ export const useNotificationsStore = defineStore("notificationsStore", () => {
loadingNotifications,
updateNotification,
updateBatchNotification,
startPollingNotifications,
startWatchingNotifications,
};
});
+2 -2
View File
@@ -4,7 +4,7 @@ import { FilesDialog } from "components/FilesDialog";
import { useGlobalUploadModal } from "composables/globalUploadModal";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import { rewatchHistory } from "store/historyStore/model/watchHistory";
import { startWatchingHistory } from "store/historyStore/model/watchHistory";
import Vue from "vue";
import { uploadPayload } from "@/utils/upload-payload.js";
@@ -116,5 +116,5 @@ export function refreshContentsWrapper() {
// Legacy Panel Interface. no-op if using new history
Galaxy?.currHistoryPanel?.refreshContents();
// Will not do anything in legacy interface
rewatchHistory();
startWatchingHistory();
}