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, payload,
storeToken, storeToken,
tokenKey, tokenKey,
pushToStore,
serializer, serializer,
store, store,
schema, schema,
@@ -213,6 +214,7 @@ export default class SqliteHandler {
payload, payload,
storeToken, storeToken,
tokenKey, tokenKey,
pushToStore,
serializer, serializer,
store, store,
schema, 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( const normalizedPayload = serializer.normalizeResponse(
store, store,
schema, schema,
@@ -239,6 +236,19 @@ export default class SqliteHandler {
const { data: payloadData } = normalizedPayload; 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 // Store the new data we just got back from the API refresh
const items = payloadData.map((datum) => { const items = payloadData.map((datum) => {
const params = Object.values(modelMapping[type]).map((key) => { const params = Object.values(modelMapping[type]).map((key) => {
@@ -98,6 +98,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
'session', 'session',
{ {
scope_id, scope_id,
include_terminated: true,
query: { query: {
filters: { filters: {
scope_id: [{ equals: scope_id }], 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 }; return { targets, doTargetsExist, totalItems };
@@ -364,11 +364,11 @@ module('Acceptance | targets | list', function (hooks) {
instances.sshTarget.id, instances.sshTarget.id,
'session is associated with correct target', 'session is associated with correct target',
); );
const emberDataSessionModel = this.owner const emberDataSessionModelBefore = this.owner
.lookup('service:store') .lookup('service:store')
.peekRecord('session', instances.session.id); .peekRecord('session', instances.session.id);
assert.strictEqual( assert.strictEqual(
emberDataSessionModel.status, emberDataSessionModelBefore.status,
STATUS_SESSION_ACTIVE, STATUS_SESSION_ACTIVE,
'ember data session model is active', 'ember data session model is active',
); );
@@ -389,6 +389,10 @@ module('Acceptance | targets | list', function (hooks) {
instances.session.status = STATUS_SESSION_TERMINATED; instances.session.status = STATUS_SESSION_TERMINATED;
await click(commonSelectors.HREF(urls.targets)); await click(commonSelectors.HREF(urls.targets));
const emberDataSessionModelAfter = this.owner
.lookup('service:store')
.peekRecord('session', instances.session.id);
assert assert
.dom(selectors.TABLE_TARGETS_ROW(instances.sshTarget.id)) .dom(selectors.TABLE_TARGETS_ROW(instances.sshTarget.id))
.exists('the target is still listed in the table'); .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)) .dom(selectors.TABLE_ACTIVE_SESSIONS(instances.sshTarget.id))
.doesNotExist('the target does not have an active session'); .doesNotExist('the target does not have an active session');
assert.strictEqual( assert.strictEqual(
emberDataSessionModel.status, emberDataSessionModelAfter.status,
STATUS_SESSION_TERMINATED, STATUS_SESSION_TERMINATED,
'the session ember data model status is updated', '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 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`. 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 Similar to running in development, you can also use `SETUP_CLI=true` to
bypass the download of the CLI, which can be useful for pre-release testing. See enable the download of the CLI, which can be useful for pre-release testing. See
[Developing Using Non-Release Versions of [Developing Using Non-Release Versions of
Boundary](#developing-using-non-release-versions-of-boundary) for more details. 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 // To correctly show targets with active sessions, the associated
// sessions need to be queried to sync all the session models in // sessions need to be queried to sync all the session models in
// ember data and retrieve their updated `status` properties // ember data and retrieve their updated `status` properties
const sessionsPromise = this.store.query( const sessionsPromise = this.store.query('session', {
'session', query: {
{ filters: {
query: { target_id: targets.map((target) => ({ equals: target.id })),
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 // Load the sessions and aliases for the targets on the current page
try { try {
await Promise.all([aliasPromise, sessionsPromise]); const loadedResults = await Promise.all([
aliasPromise,
sessionsPromise,
]);
allAssociatedSessions = loadedResults[1];
} catch (e) { } catch (e) {
__electronLog?.warn( __electronLog?.warn(
'Could not retrieve aliases and/or sessions for targets', '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 // 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 { return {
targets, targets,
projects, projects,
@@ -8,6 +8,7 @@ const {
spawnAsyncJSONPromise, spawnAsyncJSONPromise,
spawnSync, spawnSync,
} = require('../helpers/spawn-promise.js'); } = require('../helpers/spawn-promise.js');
const log = require('electron-log/main');
class Session { class Session {
#id; #id;
@@ -89,7 +90,6 @@ class Session {
).then((spawnedSession) => { ).then((spawnedSession) => {
this.#process = spawnedSession.childProcess; this.#process = spawnedSession.childProcess;
this.#proxyDetails = spawnedSession.response; this.#proxyDetails = spawnedSession.response;
this.#process = spawnedSession.childProcess;
this.#id = this.#proxyDetails.session_id; this.#id = this.#proxyDetails.session_id;
return this.#proxyDetails; return this.#proxyDetails;
}); });
@@ -102,7 +102,10 @@ class Session {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.isRunning) { if (this.isRunning) {
this.#process.on('close', () => resolve()); 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 // Cancel session before killing process
const sanitizedToken = sanitizer.base62EscapeAndValidate(this.#token); 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); assert.strictEqual(instances.session.status, STATUS_SESSION_ACTIVE);
await visit(urls.targets); await visit(urls.targets);
const emberDataSessionModel = this.owner const emberDataSessionModelBefore = this.owner
.lookup('service:store') .lookup('service:store')
.peekRecord('session', instances.session.id); .peekRecord('session', instances.session.id);
assert.strictEqual(emberDataSessionModel.status, STATUS_SESSION_ACTIVE); assert.strictEqual(
emberDataSessionModelBefore.status,
STATUS_SESSION_ACTIVE,
);
assert assert
.dom(activeSessionFlyoutButtonSelector(instances.session.targetId)) .dom(activeSessionFlyoutButtonSelector(instances.session.targetId))
@@ -695,7 +698,13 @@ module('Acceptance | projects | targets | index', function (hooks) {
await click(`[href="${urls.targets}"]`); 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 assert
.dom(activeSessionFlyoutButtonSelector(instances.session.targetId)) .dom(activeSessionFlyoutButtonSelector(instances.session.targetId))
.doesNotExist(); .doesNotExist();