Skip to content

Commit acccc3a

Browse files
Respect latest available staleness prior to time conductor start (#8211)
* add clearStaleness method since === SKIP_CHECK flag * fix logic for updating staleness * should be inclusive to start and end bounds * should respect prior to start bounds if stale * should not show staleness for after end bounds * add `ExampleStalenessProvider` update telemetry api jsdocs for staless provider * move sine wave staleness tests into appropriate folder location * convert `StateGenerator` into class * clean up coding style * use timesystem key and now() from openmct time api * fix `ExampleStalenessProvider` initial conditions install `ExampleStalenessProvider` by default in index.html * Revert "fix logic for updating staleness" To allow contribution from marcelo-earth This reverts commit 3baef16. * Refactor shouldUpdateStaleness to accept updates based on timestamp * clarify comment remove unused code * fix import paths * clean up staleness provider write e2e test for staleness * fix 404s to example imagery breaking e2e tests --------- Co-authored-by: Marcelo Arias <hello@marceloarias.com>
1 parent 4ab98ff commit acccc3a

12 files changed

Lines changed: 397 additions & 116 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2025, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
// This should be used to install the Example Staleness Provider
24+
document.addEventListener('DOMContentLoaded', () => {
25+
const openmct = window.openmct;
26+
openmct.install(openmct.plugins.example.ExampleStaleness({ stalenessInterval: 2500 }));
27+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
import {
24+
createDomainObjectWithDefaults,
25+
navigateToObjectWithRealTime
26+
} from '../../../../appActions.js';
27+
import { expect, test } from '../../../../pluginFixtures.js';
28+
29+
test.describe('Staleness', () => {
30+
test.beforeEach(async ({ page }) => {
31+
await page.goto('./', { waitUntil: 'domcontentloaded' });
32+
});
33+
34+
test('Does not show staleness after navigating from a stale object', async ({ page }) => {
35+
const staleSWG = await createDomainObjectWithDefaults(page, {
36+
type: 'Sine Wave Generator',
37+
name: 'SWG'
38+
});
39+
40+
// edit properties and enable staleness updates
41+
await page.getByLabel('More actions').click();
42+
await page.getByLabel('Edit properties...').click();
43+
await page.getByLabel('Provide Staleness Updates', { exact: true }).click();
44+
await page.getByLabel('Save').click();
45+
46+
const folder = await createDomainObjectWithDefaults(page, {
47+
type: 'Folder',
48+
name: 'Folder 1'
49+
});
50+
51+
// Navigate to the stale object
52+
await navigateToObjectWithRealTime(page, staleSWG.url);
53+
54+
// Assert that staleness is shown
55+
await expect(page.getByLabel('Object View')).toHaveClass(/is-stale/, {
56+
timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated
57+
});
58+
59+
// Immediately navigate to the folder
60+
await page.goto(folder.url);
61+
62+
// Verify that staleness is not shown
63+
await expect(page.getByLabel('Object View')).not.toHaveClass(/is-stale/);
64+
});
65+
});
Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*****************************************************************************
2-
* Open MCT, Copyright (c) 2014-2024, United States Government
2+
* Open MCT, Copyright (c) 2014-2025, United States Government
33
* as represented by the Administrator of the National Aeronautics and Space
44
* Administration. All rights reserved.
55
*
@@ -20,43 +20,49 @@
2020
* at runtime from the About dialog for additional information.
2121
*****************************************************************************/
2222

23+
import { fileURLToPath } from 'url';
24+
2325
import { createDomainObjectWithDefaults, navigateToObjectWithRealTime } from '../../appActions.js';
2426
import { expect, test } from '../../pluginFixtures.js';
2527

26-
test.describe('Staleness', () => {
27-
test.beforeEach(async ({ page }) => {
28-
await page.goto('./', { waitUntil: 'domcontentloaded' });
29-
});
28+
test.describe('Staleness with Controlled Clock @clock', () => {
29+
test.describe('Using ExampleStalenessProvider in realtime mode', () => {
30+
let objectView;
31+
let stateGenerator;
3032

31-
test('Does not show staleness after navigating from a stale object', async ({ page }) => {
32-
const staleSWG = await createDomainObjectWithDefaults(page, {
33-
type: 'Sine Wave Generator',
34-
name: 'SWG'
35-
});
33+
test.beforeEach(async ({ page }) => {
34+
objectView = page.getByLabel('Object View');
3635

37-
// edit properties and enable staleness updates
38-
await page.getByLabel('More actions').click();
39-
await page.getByLabel('Edit properties...').click();
40-
await page.getByLabel('Provide Staleness Updates', { exact: true }).click();
41-
await page.getByLabel('Save').click();
36+
await page.addInitScript({
37+
path: fileURLToPath(
38+
new URL('../../helper/addInitExampleStalenessProvider.js', import.meta.url)
39+
)
40+
});
4241

43-
const folder = await createDomainObjectWithDefaults(page, {
44-
type: 'Folder',
45-
name: 'Folder 1'
46-
});
42+
// Go to baseURL
43+
await page.goto('./', { waitUntil: 'domcontentloaded' });
4744

48-
// Navigate to the stale object
49-
await navigateToObjectWithRealTime(page, staleSWG.url);
45+
// Create a state generator object, since it can have sparse data
46+
stateGenerator = await createDomainObjectWithDefaults(page, {
47+
type: 'State Generator',
48+
name: 'Test State Generator'
49+
});
5050

51-
// Assert that staleness is shown
52-
await expect(page.getByLabel('Object View')).toHaveClass(/is-stale/, {
53-
timeout: 30 * 1000 // Give 30 seconds for the staleness to be updated
51+
await navigateToObjectWithRealTime(page, stateGenerator.url);
5452
});
5553

56-
// Immediately navigate to the folder
57-
await page.goto(folder.url);
58-
59-
// Verify that staleness is not shown
60-
await expect(page.getByLabel('Object View')).not.toHaveClass(/is-stale/);
54+
test('indicates when telemetry is stale and clears staleness when telemetry is not stale', async ({
55+
page
56+
}) => {
57+
await expect(objectView).toHaveClass(/is-stale/, {
58+
timeout: 5 * 1000 // Give 3 seconds for the staleness to be updated
59+
});
60+
await expect(objectView).not.toHaveClass(/is-stale/, {
61+
timeout: 5 * 1000 // Give 3 seconds for the staleness to be updated
62+
});
63+
await expect(objectView).toHaveClass(/is-stale/, {
64+
timeout: 5 * 1000 // Give 3 seconds for the staleness to be updated
65+
});
66+
});
6167
});
6268
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2025, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
/**
24+
* @implements {import('src/api/telemetry/TelemetryAPI').StalenessProvider}
25+
*/
26+
export default class ExampleStalenessProvider {
27+
#intervalId;
28+
constructor(openmct, config = { stalenessInterval: 3000, reportStalenessInterval: 300 }) {
29+
this.openmct = openmct;
30+
this.stalenessInterval = config.stalenessInterval;
31+
this.reportStalenessInterval = config.reportStalenessInterval;
32+
this.observingStaleness = {};
33+
this.latestReceivedTelemetry = {};
34+
35+
this.#observeTimeSystem();
36+
this.#observeStaleness();
37+
}
38+
39+
#observeTimeSystem() {
40+
this.openmct.time.on('timeSystemChanged', () => {
41+
this.timeSystem = this.openmct.time.getTimeSystem();
42+
});
43+
}
44+
45+
supportsStaleness(domainObject) {
46+
return this.openmct.telemetry.isTelemetryObject(domainObject);
47+
}
48+
49+
subscribeToStaleness(domainObject, callback) {
50+
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
51+
52+
this.observingStaleness[keyString] = { callback };
53+
const unsubscribe = this.openmct.telemetry.subscribe(domainObject, (datum) => {
54+
this.#updateLatestReceivedTelemetry(domainObject, datum);
55+
});
56+
57+
return () => {
58+
delete this.observingStaleness[keyString];
59+
unsubscribe?.();
60+
if (Object.keys(this.observingStaleness).length === 0) {
61+
clearInterval(this.#intervalId);
62+
}
63+
};
64+
}
65+
66+
#observeStaleness() {
67+
this.#intervalId = setInterval(() => {
68+
if (!this.timeSystem) {
69+
return;
70+
}
71+
72+
Object.entries(this.observingStaleness).forEach(([keyString, observer]) => {
73+
if (!this.latestReceivedTelemetry[keyString]) {
74+
return;
75+
}
76+
77+
const now = this.openmct.time.now();
78+
const isStale = now - this.latestReceivedTelemetry[keyString] >= this.stalenessInterval;
79+
80+
// Overly reports when not stale because of generated telemetry flake
81+
if (!isStale || !observer.response || isStale !== observer.response.isStale) {
82+
const stalenessResponseObject = {
83+
isStale,
84+
[this.timeSystem.key]: now
85+
};
86+
87+
observer.response = stalenessResponseObject;
88+
observer.callback(stalenessResponseObject);
89+
}
90+
});
91+
}, this.reportStalenessInterval);
92+
}
93+
94+
/**
95+
* @param {*} domainObject
96+
* @returns {import('src/api/telemetry/TelemetryAPI').StalenessResponseObject}
97+
*/
98+
async isStale(domainObject) {
99+
if (!this.timeSystem) {
100+
this.timeSystem = this.openmct.time.getTimeSystem();
101+
}
102+
103+
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
104+
105+
if (!this.latestReceivedTelemetry[keyString]) {
106+
// Naively assumes sorted request response so uses last datum in array
107+
const response = await this.openmct.telemetry.request(domainObject, { strategy: 'latest' });
108+
const lastDatum = response?.length ? response[response.length - 1] : undefined;
109+
this.#updateLatestReceivedTelemetry(domainObject, lastDatum);
110+
}
111+
112+
const timestamp = this.latestReceivedTelemetry[keyString];
113+
if (timestamp) {
114+
const isStale = this.openmct.time.now() - timestamp >= this.stalenessInterval;
115+
116+
const stalenessResponseObject = { isStale };
117+
stalenessResponseObject[this.timeSystem.key] = timestamp;
118+
119+
return stalenessResponseObject;
120+
}
121+
}
122+
123+
#updateLatestReceivedTelemetry(domainObject, datum) {
124+
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
125+
const metadata = this.openmct.telemetry.getMetadata(domainObject);
126+
const metadataValue = metadata.value(this.timeSystem.key) || { format: this.timeSystem.key };
127+
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
128+
const timestamp = valueFormatter.parse(datum);
129+
130+
if (timestamp) {
131+
this.latestReceivedTelemetry[keyString] = timestamp;
132+
} else {
133+
console.warn('Could not parse timestamp for staleness check');
134+
}
135+
}
136+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2025, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
import ExampleStalenessProvider from './ExampleStalenessProvider.js';
24+
25+
export default function ExampleStalenessPlugin(config) {
26+
return function install(openmct) {
27+
const stalenessProvider = new ExampleStalenessProvider(openmct, config);
28+
openmct.telemetry.addProvider(stalenessProvider);
29+
};
30+
}

0 commit comments

Comments
 (0)