Skip to content

Commit

Permalink
Merge pull request galaxyproject#17355 from davelopez/unify_continuou…
Browse files Browse the repository at this point in the history
…s_polling

Unify continuous polling with composable
  • Loading branch information
davelopez authored Jan 30, 2024
2 parents 431429c + f2d4484 commit 24b90be
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 107 deletions.
4 changes: 2 additions & 2 deletions client/src/components/History/CurrentHistory/HistoryPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -277,7 +277,7 @@ async function onUnhide(item: HistoryItem) {
}
function reloadContents() {
rewatchHistory();
startWatchingHistory();
}
function setInvisible(item: HistoryItem) {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/History/adapters/HistoryPanelProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -26,7 +26,7 @@ export class HistoryPanelProxy {
};

// start watching the history with continuous queries
watchHistory();
startWatchingHistory();
}

syncCurrentHistoryModel(currentHistory) {
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/InteractiveTools/InteractiveTools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,8 @@ export default {
this.load();
},
methods: {
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints", "removeEntryPoint"]),
...mapActions(useEntryPointStore, ["removeEntryPoint"]),
load() {
this.ensurePollingEntryPoints();
this.filter = "";
},
filtered: function (items) {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Masthead/Masthead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ watch(
/* lifecyle */
onBeforeMount(() => {
entryPointStore = useEntryPointStore();
entryPointStore.ensurePollingEntryPoints();
entryPointStore.startWatchingEntryPoints();
entryPointStore.$subscribe((mutation, state) => {
updateVisibility(state.entryPoints.length > 0);
});
Expand Down
8 changes: 1 addition & 7 deletions client/src/components/ToolEntryPoints/ToolEntryPoints.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -62,11 +62,5 @@ export default {
return getAppRoot() + "interactivetool_entry_points/list";
},
},
created: function () {
this.ensurePollingEntryPoints();
},
methods: {
...mapActions(useEntryPointStore, ["ensurePollingEntryPoints"]),
},
};
</script>
94 changes: 94 additions & 0 deletions client/src/composables/resourceWatcher.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
6 changes: 3 additions & 3 deletions client/src/entry/analysis/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
},
Expand All @@ -189,9 +189,9 @@ export default {
}
},
methods: {
startNotificationsPolling() {
startWatchingNotifications() {
const notificationsStore = useNotificationsStore();
notificationsStore.startPollingNotifications();
notificationsStore.startWatchingNotifications();
},
openUrl(urlObj) {
if (!urlObj.target) {
Expand Down
62 changes: 19 additions & 43 deletions client/src/store/historyStore/model/watchHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,37 @@ 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;

// 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;
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions client/src/store/historyStore/model/watchHistory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
});
Expand Down
11 changes: 1 addition & 10 deletions client/src/stores/entryPointStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,15 @@ 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();
});

afterEach(() => {
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",
Expand Down
Loading

0 comments on commit 24b90be

Please sign in to comment.