Unload expired sessions from store (#3033)

* fix: 🐛 unload expired sessions from ember data store

* fix: 🐛 add unloadAll & include terminated to admin ui

* test: 💍 update tests

* refactor: 💡 pr feedback for admin ui

* refactor: 💡 remove unneeded query from desktop ui

* refactor: 💡 check pushToStore var

* Revert "refactor: 💡 remove unneeded query from desktop ui"

* fix: 🐛 fix proxy details being removed after unload

* refactor: 💡 remove log
This commit is contained in:
Lisbet Alvarez authored and GitHub committed 2025-11-11 11:16:12 -08:00
1 parent 58cc9e56fc
commit 48d0087141
7 files changed
+75 -26

No files matched your search

+15 -5
View File
@@ -107,6 +107,7 @@ export default class SqliteHandler {
payload,
storeToken,
tokenKey,
pushToStore,
serializer,
store,
schema,
@@ -213,6 +214,7 @@ export default class SqliteHandler {
payload,
storeToken,
tokenKey,
pushToStore,
serializer,
store,
schema,
@@ -224,11 +226,6 @@ export default class SqliteHandler {
]);
}
// Remove any records from the DB if the API indicates they've been deleted
if (payload.removed_ids?.length > 0) {
await this.sqlite.deleteResource(type, payload.removed_ids);
}
const normalizedPayload = serializer.normalizeResponse(
store,
schema,
@@ -239,6 +236,19 @@ export default class SqliteHandler {
const { data: payloadData } = normalizedPayload;
// Remove any records from the DB if the API indicates they've been deleted
// Additionally remove deleted records from ember data store for symmetry
if (payload.removed_ids?.length > 0) {
await this.sqlite.deleteResource(type, payload.removed_ids);
if (pushToStore) {
payload.removed_ids.forEach((id) => {
const record = store.peekRecord(type, id);
store.unloadRecord(record);
});
}
}
// Store the new data we just got back from the API refresh
const items = payloadData.map((datum) => {
const params = Object.values(modelMapping[type]).map((key) => {
@@ -98,6 +98,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
'session',
{
scope_id,
include_terminated: true,
query: {
filters: {
scope_id: [{ equals: scope_id }],
@@ -155,7 +156,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
},
},
},
{ pushToStore: true, peekDb: true },
{ peekDb: true },
);
}
return { targets, doTargetsExist, totalItems };
@@ -364,11 +364,11 @@ module('Acceptance | targets | list', function (hooks) {
instances.sshTarget.id,
'session is associated with correct target',
);
const emberDataSessionModel = this.owner
const emberDataSessionModelBefore = this.owner
.lookup('service:store')
.peekRecord('session', instances.session.id);
assert.strictEqual(
emberDataSessionModel.status,
emberDataSessionModelBefore.status,
STATUS_SESSION_ACTIVE,
'ember data session model is active',
);
@@ -389,6 +389,10 @@ module('Acceptance | targets | list', function (hooks) {
instances.session.status = STATUS_SESSION_TERMINATED;
await click(commonSelectors.HREF(urls.targets));
const emberDataSessionModelAfter = this.owner
.lookup('service:store')
.peekRecord('session', instances.session.id);
assert
.dom(selectors.TABLE_TARGETS_ROW(instances.sshTarget.id))
.exists('the target is still listed in the table');
@@ -396,7 +400,7 @@ module('Acceptance | targets | list', function (hooks) {
.dom(selectors.TABLE_ACTIVE_SESSIONS(instances.sshTarget.id))
.doesNotExist('the target does not have an active session');
assert.strictEqual(
emberDataSessionModel.status,
emberDataSessionModelAfter.status,
STATUS_SESSION_TERMINATED,
'the session ember data model status is updated',
);
+2 -2
View File
@@ -122,8 +122,8 @@ The static production assets are saved into the `dist/` folder.
The Boundary CLI is downloaded and extracted to `electron-app/cli/` folder as part of
packaging. CLI version is defined in `electron-app/config/cli.js`.
Similar to running in development, you can also use `BYPASS_CLI_SETUP=true` to
bypass the download of the CLI, which can be useful for pre-release testing. See
Similar to running in development, you can also use `SETUP_CLI=true` to
enable the download of the CLI, which can be useful for pre-release testing. See
[Developing Using Non-Release Versions of
Boundary](#developing-using-non-release-versions-of-boundary) for more details.
@@ -159,21 +159,22 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
// To correctly show targets with active sessions, the associated
// sessions need to be queried to sync all the session models in
// ember data and retrieve their updated `status` properties
const sessionsPromise = this.store.query(
'session',
{
query: {
filters: {
target_id: targets.map((target) => ({ equals: target.id })),
},
const sessionsPromise = this.store.query('session', {
query: {
filters: {
target_id: targets.map((target) => ({ equals: target.id })),
},
},
{ pushToStore: true },
);
});
let allAssociatedSessions;
// Load the sessions and aliases for the targets on the current page
try {
await Promise.all([aliasPromise, sessionsPromise]);
const loadedResults = await Promise.all([
aliasPromise,
sessionsPromise,
]);
allAssociatedSessions = loadedResults[1];
} catch (e) {
__electronLog?.warn(
'Could not retrieve aliases and/or sessions for targets',
@@ -183,6 +184,27 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
// the page in case the controller doesn't support aliases yet
}
// Remove expired sessions in ember data store for symmetry with cache.
if (allAssociatedSessions) {
const targetIds = targets.map(({ id }) => id);
const allAssociatedSessionIds = new Set(
allAssociatedSessions.map(({ id }) => id),
);
const storedSessionIds = new Set(
this.store
.peekAll('session')
.filter((s) => targetIds.includes(s?.target_id))
.map(({ id }) => id),
);
const removedSessions = storedSessionIds.difference(
allAssociatedSessionIds,
);
removedSessions.forEach((id) => {
const record = this.store.peekRecord('session', id);
this.store.unloadRecord(record);
});
}
return {
targets,
projects,
@@ -8,6 +8,7 @@ const {
spawnAsyncJSONPromise,
spawnSync,
} = require('../helpers/spawn-promise.js');
const log = require('electron-log/main');
class Session {
#id;
@@ -89,7 +90,6 @@ class Session {
).then((spawnedSession) => {
this.#process = spawnedSession.childProcess;
this.#proxyDetails = spawnedSession.response;
this.#process = spawnedSession.childProcess;
this.#id = this.#proxyDetails.session_id;
return this.#proxyDetails;
});
@@ -102,7 +102,10 @@ class Session {
return new Promise((resolve, reject) => {
if (this.isRunning) {
this.#process.on('close', () => resolve());
this.#process.on('error', (e) => reject(e));
this.#process.on('error', (e) => {
log.error('Process error in session stop method: ', e);
return reject(e);
});
// Cancel session before killing process
const sanitizedToken = sanitizer.base62EscapeAndValidate(this.#token);
@@ -673,11 +673,14 @@ module('Acceptance | projects | targets | index', function (hooks) {
assert.strictEqual(instances.session.status, STATUS_SESSION_ACTIVE);
await visit(urls.targets);
const emberDataSessionModel = this.owner
const emberDataSessionModelBefore = this.owner
.lookup('service:store')
.peekRecord('session', instances.session.id);
assert.strictEqual(emberDataSessionModel.status, STATUS_SESSION_ACTIVE);
assert.strictEqual(
emberDataSessionModelBefore.status,
STATUS_SESSION_ACTIVE,
);
assert
.dom(activeSessionFlyoutButtonSelector(instances.session.targetId))
@@ -695,7 +698,13 @@ module('Acceptance | projects | targets | index', function (hooks) {
await click(`[href="${urls.targets}"]`);
assert.strictEqual(emberDataSessionModel.status, STATUS_SESSION_TERMINATED);
const emberDataSessionModelAfter = this.owner
.lookup('service:store')
.peekRecord('session', instances.session.id);
assert.strictEqual(
emberDataSessionModelAfter.status,
STATUS_SESSION_TERMINATED,
);
assert
.dom(activeSessionFlyoutButtonSelector(instances.session.targetId))
.doesNotExist();