Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/@webex/internal-plugin-device/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export const FEATURE_TYPES = {
export const DEVICE_EVENT_REGISTRATION_SUCCESS = 'registration:success';

export const DEVICE_EVENTS = [DEVICE_EVENT_REGISTRATION_SUCCESS];

// Device deletion constants.
export const MIN_DEVICES_FOR_CLEANUP = 5;
export const MAX_DELETION_CONFIRMATION_ATTEMPTS = 5;
export const DELETION_CONFIRMATION_DELAY_MS = 3000;
133 changes: 107 additions & 26 deletions packages/@webex/internal-plugin-device/src/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {orderBy} from 'lodash';
import uuid from 'uuid';

import METRICS from './metrics';
import {FEATURE_COLLECTION_NAMES, DEVICE_EVENT_REGISTRATION_SUCCESS} from './constants';
import {
FEATURE_COLLECTION_NAMES,
DEVICE_EVENT_REGISTRATION_SUCCESS,
MIN_DEVICES_FOR_CLEANUP,
MAX_DELETION_CONFIRMATION_ATTEMPTS,
DELETION_CONFIRMATION_DELAY_MS,
} from './constants';
import FeaturesModel from './features/features-model';
import IpNetworkDetector from './ipNetworkDetector';
import {CatalogDetails} from './types';
Expand Down Expand Up @@ -454,46 +460,117 @@ const Device = WebexPlugin.extend({
});
},
/**
* Fetches the web devices and deletes the third of them which are not recent devices in use
* @returns {Promise<void, Error>}
* Fetches devices matching the current device type.
* @returns {Promise<Array>} filtered device list
*/
deleteDevices() {
// Fetch devices with a GET request
_getDevicesOfCurrentType() {
const {deviceType} = this._getBody();

return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
}).then((response) => response.body.devices.filter((item) => item.deviceType === deviceType));
},

const {deviceType} = this._getBody();
/**
* Waits until the server-side device count drops to or below targetCount,
* polling up to maxAttempts times with a delay between each check.
* @param {number} targetCount - resolve when device count drops to this value or below
* @param {number} [attempt=0]
* @returns {Promise<void>}
*/
_waitForDeviceCountBelowLimit(targetCount, attempt = 0) {
if (attempt >= MAX_DELETION_CONFIRMATION_ATTEMPTS) {
this.logger.warn('device: max confirmation attempts reached, proceeding anyway');

return Promise.resolve();
}

return new Promise((resolve) => setTimeout(resolve, DELETION_CONFIRMATION_DELAY_MS))
.then(() => this._getDevicesOfCurrentType())
.then((devices) => {
Comment thread
Coread marked this conversation as resolved.
this.logger.info(
`device: confirmation check ${attempt + 1}/${MAX_DELETION_CONFIRMATION_ATTEMPTS}, ` +
`${devices.length} devices remaining (target: ≤ ${targetCount})`
);

if (devices.length <= targetCount) {
this.logger.info('device: device count is now safely below limit');

return Promise.resolve();
}

return this._waitForDeviceCountBelowLimit(targetCount, attempt + 1);
})
.catch((error) => {
this.logger.warn(
`device: confirmation check ${attempt + 1} failed, proceeding anyway:`,
error
);

return Promise.resolve();
});
},

// Filter devices of type deviceType
const webDevices = devices.filter((item) => item.deviceType === deviceType);
/**
* Fetches the web devices and deletes the oldest third, then waits
* for the server to confirm the count is below the limit.
* @returns {Promise<void>}
*/
deleteDevices() {
let targetCount;

return this._getDevicesOfCurrentType()
.then((webDevices) => {
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

return Promise.race(
urlsToDelete.map((url) => {
return this.request({
uri: url,
method: 'DELETE',
});
})
if (sortedDevices.length <= MIN_DEVICES_FOR_CLEANUP) {
this.logger.info(
`device: only ${sortedDevices.length} devices found (minimum ${MIN_DEVICES_FOR_CLEANUP}), skipping cleanup`
);

return Promise.resolve();
Comment on lines +528 to +533
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delete at least one device in excessive-registration recovery

When registration fails with User has excessive device registrations, register() relies on deleteDevices() to free capacity before the single retry. This new guard returns early for <= 5 same-type devices, so users with mixed device fleets (for example, 101 total devices but only 3 WEB entries) perform no cleanup and the retry is very likely to fail again. This is a regression from the previous behavior (>2 threshold) that could block registration in production for accounts near the global limit.

Useful? React with 👍 / 👎.

}

return Promise.resolve();
const devicesToDelete = sortedDevices.slice(0, Math.ceil(sortedDevices.length / 3));
targetCount = sortedDevices.length - Math.min(5, devicesToDelete.length);
Comment thread
Coread marked this conversation as resolved.
Comment thread
Coread marked this conversation as resolved.

this.logger.info(
`device: deleting ${devicesToDelete.length} of ${webDevices.length} devices`
);

return Promise.all(
devicesToDelete.map((device) =>
this.request({uri: device.url, method: 'DELETE'})
.then(() => ({status: 'fulfilled'}))
.catch((reason) => ({status: 'rejected', reason}))
)
).then((results) => {
const failed = results.filter((r) => r.status === 'rejected');

if (failed.length > 0) {
this.logger.warn(
`device: ${failed.length} of ${devicesToDelete.length} deletions failed (best-effort, continuing)`
);
}
this.logger.info(
`device: deleted ${devicesToDelete.length - failed.length} of ${
devicesToDelete.length
} devices`
);
});
})
.then(() =>
targetCount !== undefined
? this._waitForDeviceCountBelowLimit(targetCount, 0)
: Promise.resolve()
)
.then(() => {
this.logger.info('device: device count confirmed below limit, cleanup successful');
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
this.logger.error('device: failed to delete devices:', error);

return Promise.reject(error);
});
Expand All @@ -519,7 +596,11 @@ const Device = WebexPlugin.extend({

return this._registerInternal(deviceRegistrationOptions).catch((error) => {
if (error?.body?.message === 'User has excessive device registrations') {
this.logger.info('device: excessive device registrations detected, initiating cleanup');

return this.deleteDevices().then(() => {
this.logger.info('device: device cleanup complete, retrying registration');

return this._registerInternal(deviceRegistrationOptions);
});
}
Expand Down
Loading
Loading