Skip to content

Commit

Permalink
feat: script to perform cold start of the poller
Browse files Browse the repository at this point in the history
  • Loading branch information
dicagno committed Jan 28, 2025
1 parent 91f1bac commit 378306a
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 0 deletions.
44 changes: 44 additions & 0 deletions actions/state-manager/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const stateLib = require('@adobe/aio-lib-state');

async function main({ op, key, value }) {
const state = await stateLib.init();
let result;

switch (op) {
case 'get':
result = await state.get(key);
break;
case 'put':
result = await state.put(key, value);
break;
case 'delete':
result = await state.delete(key);
break;
case 'stats':
result = await state.stats();
// eslint-disable-next-line no-fallthrough
case 'list':
default: {
result = [];
for await (const { keys } of state.list()) {
result.push(...keys);
}
}
}

return { op, key, result };
}

exports.main = main;
187 changes: 187 additions & 0 deletions test/refresh-pdps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

// Mock modules before requiring the main file
const mockOpenWhiskInstance = {
actions: {
invoke: jest.fn()
},
rules: {
disable: jest.fn(),
enable: jest.fn()
}
};

jest.mock('openwhisk', () => {
return jest.fn(() => mockOpenWhiskInstance);
});

jest.mock('dotenv', () => ({
config: jest.fn()
}));

jest.mock('commander', () => {
const mockProgram = {
option: jest.fn().mockReturnThis(),
parse: jest.fn().mockReturnThis(),
opts: jest.fn().mockReturnValue({ keys: 'en,fr' })
};
return { program: mockProgram };
});
// Store original process.env
const originalEnv = process.env;

describe('Refresh PDP Tool Tests', () => {
let mainModule;

beforeEach(() => {
// Reset process.env before each test
process.env = { ...originalEnv };
process.env.AIO_RUNTIME_NAMESPACE = 'test-namespace';
process.env.AIO_RUNTIME_AUTH = 'test-auth';

// Clear all mocks
jest.clearAllMocks();
jest.resetModules();

// Reset mock functions
mockOpenWhiskInstance.actions.invoke.mockReset();
mockOpenWhiskInstance.rules.disable.mockReset();
mockOpenWhiskInstance.rules.enable.mockReset();

// Mock exit and console
process.exit = jest.fn();
console.log = jest.fn();
console.error = jest.fn();
});

afterEach(() => {
process.env = originalEnv;
});

describe('Environment Variables', () => {
it('should exit if AIO_RUNTIME_NAMESPACE is missing', () => {
delete process.env.AIO_RUNTIME_NAMESPACE;
require('../tools/refresh-pdps');

expect(process.exit).toHaveBeenCalledWith(1);
expect(console.log).toHaveBeenCalledWith(
'Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE'
);
});

it('should exit if AIO_RUNTIME_AUTH is missing', () => {
delete process.env.AIO_RUNTIME_AUTH;
require('../tools/refresh-pdps');

expect(process.exit).toHaveBeenCalledWith(1);
expect(console.log).toHaveBeenCalledWith(
'Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE'
);
});
});

describe('checkState function', () => {
beforeEach(() => {
mainModule = require('../tools/refresh-pdps');
});

it('should return state value for given locale', async () => {
const expectedState = { value: 'false' };
mockOpenWhiskInstance.actions.invoke.mockResolvedValueOnce(expectedState);

const result = await mainModule.checkState('en');

expect(mockOpenWhiskInstance.actions.invoke).toHaveBeenCalledWith({
name: 'state-manager',
params: { key: 'en', op: 'get' },
blocking: true,
result: true
});
expect(result).toBe('false');
});

it('should handle errors when checking state', async () => {
mockOpenWhiskInstance.actions.invoke.mockRejectedValueOnce(new Error('API Error'));
await expect(mainModule.checkState('en')).rejects.toThrow('API Error');
});
});

describe('flushStoreState function', () => {
beforeEach(() => {
mainModule = require('../tools/refresh-pdps');
});

it('should invoke delete operation for given locale', async () => {
await mainModule.flushStoreState('en');

expect(mockOpenWhiskInstance.actions.invoke).toHaveBeenCalledWith({
name: 'state-manager',
params: { key: 'en', op: 'delete' },
blocking: true,
result: true
});
});

it('should handle errors when flushing state', async () => {
mockOpenWhiskInstance.actions.invoke.mockRejectedValueOnce(new Error('Delete Error'));
await expect(mainModule.flushStoreState('en')).rejects.toThrow('Delete Error');
});
});

describe('main function', () => {
beforeEach(() => {
mainModule = require('../tools/refresh-pdps');
mockOpenWhiskInstance.rules.disable.mockResolvedValue({});
mockOpenWhiskInstance.rules.enable.mockResolvedValue({});
mockOpenWhiskInstance.actions.invoke
.mockResolvedValueOnce({ value: 'false' })
.mockResolvedValueOnce({})
.mockResolvedValueOnce({})
.mockResolvedValueOnce({ value: 'true' });

jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('should successfully complete the state management cycle', async () => {
const mainPromise = mainModule.main();
jest.runAllTimers();
await mainPromise;

expect(mockOpenWhiskInstance.rules.disable).toHaveBeenCalledWith({
name: 'poll_every_minute'
});
expect(mockOpenWhiskInstance.rules.enable).toHaveBeenCalledWith({
name: 'poll_every_minute'
});
expect(process.exit).toHaveBeenCalledWith(0);
});

it('should timeout if running state never becomes true', async () => {
mockOpenWhiskInstance.actions.invoke.mockResolvedValue({ value: 'false' });

const timeout = 1500; // to simulate in the test env
const mainPromise = mainModule.main(timeout);
jest.advanceTimersByTime(1500);
await mainPromise;

expect(console.error).toHaveBeenCalledWith(
'Timeout: running state did not become true within 30 minutes.'
);
expect(process.exit).toHaveBeenCalledWith(1);
});
});
});
9 changes: 9 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This directory contains utility scripts for various tasks related to the AppBuilder package. Below are the details on how to use each script.

## `refresh-pdps.js`

This script gracefully pauses execution of the poller, and performs a 'cold start': the poller will then re-preview and re-publish all the products in the catalog.
The operation happens in the background and the script checks that the cold start was successfully triggered, exiting 0.

### Usage

`-l, --locales <locales>: Comma-separated list of locales (or stores) to delete. For example: en,fr,de`

## `check-products-count.js`

This script checks the product count consistency between the `published-products-index` and the Adobe Commerce store.
Expand Down
125 changes: 125 additions & 0 deletions tools/refresh-pdps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const openwhisk = require('openwhisk');
const { program } = require('commander');
const { exit } = require('process');

require('dotenv').config();

const {
AIO_RUNTIME_NAMESPACE,
AIO_RUNTIME_AUTH,
} = process.env;

if (!AIO_RUNTIME_NAMESPACE || !AIO_RUNTIME_AUTH) {
console.log('Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE');
exit(1);
}

const [AIO_STATE_MANAGER_ACTION_NAME, AIO_POLLER_RULE_NAME] = ['state-manager', 'poll_every_minute'];

const ow = openwhisk({
apihost: 'https://adobeioruntime.net',
api_key: AIO_RUNTIME_AUTH,
namespace: AIO_RUNTIME_NAMESPACE,
});

let targetLocales = [];

program
.requiredOption('-l, --locales <locales>', 'Comma-separated list of locales (or stores) to target. For example: en,fr,de')
.parse(process.argv);

const options = program.opts();

if (!options.keys) {
console.error('No locales provided to delete.');
exit(1);
}

targetLocales = options.keys.split(',');

async function checkState(locale) {
const state = await ow.actions.invoke({
name: AIO_STATE_MANAGER_ACTION_NAME,
params: { key: locale, op: 'get' },
blocking: true,
result: true,
});
return state.value;
}

async function flushStoreState(locale) {
await ow.actions.invoke({
name: AIO_STATE_MANAGER_ACTION_NAME,
params: { key: locale, op: 'delete' },
blocking: true,
result: true,
});
}

async function main(timeout = 20 * 60 * 1000) {



try {
// Disable the rule
await ow.rules.disable({ name: AIO_POLLER_RULE_NAME });

// Wait until 'running' is not 'true'
let running = await checkState('running');
while (running === 'true') {
console.log('Waiting for running state to be false...');
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
running = await checkState('running');
}

// Delete specified keys
for (const key of targetLocales) {
await flushStoreState(key);
}

// Re-enable the rule to trigger the poller cycle start
await ow.rules.enable({ name: AIO_POLLER_RULE_NAME });

// Check periodically until 'running' is 'true'
const startTime = Date.now();
while (true) {
running = await checkState('running');
if (running === 'true') {
console.log('Running state is true. Exiting...');
exit(0);
}
if (Date.now() - startTime > timeout) {
console.error('Timeout: running state did not become true within 30 minutes.');
exit(1);
}
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
}
} catch (error) {
console.error('Error:', error);
exit(1);
}
}

module.exports = {
checkState,
flushStoreState,
main,
targetLocales,
};

// Only call main if this file is being run directly
if (require.main === module) {
main();
}

0 comments on commit 378306a

Please sign in to comment.