Skip to content

Commit 378306a

Browse files
committed
feat: script to perform cold start of the poller
1 parent 91f1bac commit 378306a

File tree

4 files changed

+365
-0
lines changed

4 files changed

+365
-0
lines changed

actions/state-manager/index.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
const stateLib = require('@adobe/aio-lib-state');
14+
15+
async function main({ op, key, value }) {
16+
const state = await stateLib.init();
17+
let result;
18+
19+
switch (op) {
20+
case 'get':
21+
result = await state.get(key);
22+
break;
23+
case 'put':
24+
result = await state.put(key, value);
25+
break;
26+
case 'delete':
27+
result = await state.delete(key);
28+
break;
29+
case 'stats':
30+
result = await state.stats();
31+
// eslint-disable-next-line no-fallthrough
32+
case 'list':
33+
default: {
34+
result = [];
35+
for await (const { keys } of state.list()) {
36+
result.push(...keys);
37+
}
38+
}
39+
}
40+
41+
return { op, key, result };
42+
}
43+
44+
exports.main = main;

test/refresh-pdps.test.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
// Mock modules before requiring the main file
14+
const mockOpenWhiskInstance = {
15+
actions: {
16+
invoke: jest.fn()
17+
},
18+
rules: {
19+
disable: jest.fn(),
20+
enable: jest.fn()
21+
}
22+
};
23+
24+
jest.mock('openwhisk', () => {
25+
return jest.fn(() => mockOpenWhiskInstance);
26+
});
27+
28+
jest.mock('dotenv', () => ({
29+
config: jest.fn()
30+
}));
31+
32+
jest.mock('commander', () => {
33+
const mockProgram = {
34+
option: jest.fn().mockReturnThis(),
35+
parse: jest.fn().mockReturnThis(),
36+
opts: jest.fn().mockReturnValue({ keys: 'en,fr' })
37+
};
38+
return { program: mockProgram };
39+
});
40+
// Store original process.env
41+
const originalEnv = process.env;
42+
43+
describe('Refresh PDP Tool Tests', () => {
44+
let mainModule;
45+
46+
beforeEach(() => {
47+
// Reset process.env before each test
48+
process.env = { ...originalEnv };
49+
process.env.AIO_RUNTIME_NAMESPACE = 'test-namespace';
50+
process.env.AIO_RUNTIME_AUTH = 'test-auth';
51+
52+
// Clear all mocks
53+
jest.clearAllMocks();
54+
jest.resetModules();
55+
56+
// Reset mock functions
57+
mockOpenWhiskInstance.actions.invoke.mockReset();
58+
mockOpenWhiskInstance.rules.disable.mockReset();
59+
mockOpenWhiskInstance.rules.enable.mockReset();
60+
61+
// Mock exit and console
62+
process.exit = jest.fn();
63+
console.log = jest.fn();
64+
console.error = jest.fn();
65+
});
66+
67+
afterEach(() => {
68+
process.env = originalEnv;
69+
});
70+
71+
describe('Environment Variables', () => {
72+
it('should exit if AIO_RUNTIME_NAMESPACE is missing', () => {
73+
delete process.env.AIO_RUNTIME_NAMESPACE;
74+
require('../tools/refresh-pdps');
75+
76+
expect(process.exit).toHaveBeenCalledWith(1);
77+
expect(console.log).toHaveBeenCalledWith(
78+
'Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE'
79+
);
80+
});
81+
82+
it('should exit if AIO_RUNTIME_AUTH is missing', () => {
83+
delete process.env.AIO_RUNTIME_AUTH;
84+
require('../tools/refresh-pdps');
85+
86+
expect(process.exit).toHaveBeenCalledWith(1);
87+
expect(console.log).toHaveBeenCalledWith(
88+
'Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE'
89+
);
90+
});
91+
});
92+
93+
describe('checkState function', () => {
94+
beforeEach(() => {
95+
mainModule = require('../tools/refresh-pdps');
96+
});
97+
98+
it('should return state value for given locale', async () => {
99+
const expectedState = { value: 'false' };
100+
mockOpenWhiskInstance.actions.invoke.mockResolvedValueOnce(expectedState);
101+
102+
const result = await mainModule.checkState('en');
103+
104+
expect(mockOpenWhiskInstance.actions.invoke).toHaveBeenCalledWith({
105+
name: 'state-manager',
106+
params: { key: 'en', op: 'get' },
107+
blocking: true,
108+
result: true
109+
});
110+
expect(result).toBe('false');
111+
});
112+
113+
it('should handle errors when checking state', async () => {
114+
mockOpenWhiskInstance.actions.invoke.mockRejectedValueOnce(new Error('API Error'));
115+
await expect(mainModule.checkState('en')).rejects.toThrow('API Error');
116+
});
117+
});
118+
119+
describe('flushStoreState function', () => {
120+
beforeEach(() => {
121+
mainModule = require('../tools/refresh-pdps');
122+
});
123+
124+
it('should invoke delete operation for given locale', async () => {
125+
await mainModule.flushStoreState('en');
126+
127+
expect(mockOpenWhiskInstance.actions.invoke).toHaveBeenCalledWith({
128+
name: 'state-manager',
129+
params: { key: 'en', op: 'delete' },
130+
blocking: true,
131+
result: true
132+
});
133+
});
134+
135+
it('should handle errors when flushing state', async () => {
136+
mockOpenWhiskInstance.actions.invoke.mockRejectedValueOnce(new Error('Delete Error'));
137+
await expect(mainModule.flushStoreState('en')).rejects.toThrow('Delete Error');
138+
});
139+
});
140+
141+
describe('main function', () => {
142+
beforeEach(() => {
143+
mainModule = require('../tools/refresh-pdps');
144+
mockOpenWhiskInstance.rules.disable.mockResolvedValue({});
145+
mockOpenWhiskInstance.rules.enable.mockResolvedValue({});
146+
mockOpenWhiskInstance.actions.invoke
147+
.mockResolvedValueOnce({ value: 'false' })
148+
.mockResolvedValueOnce({})
149+
.mockResolvedValueOnce({})
150+
.mockResolvedValueOnce({ value: 'true' });
151+
152+
jest.useFakeTimers();
153+
});
154+
155+
afterEach(() => {
156+
jest.useRealTimers();
157+
});
158+
159+
it('should successfully complete the state management cycle', async () => {
160+
const mainPromise = mainModule.main();
161+
jest.runAllTimers();
162+
await mainPromise;
163+
164+
expect(mockOpenWhiskInstance.rules.disable).toHaveBeenCalledWith({
165+
name: 'poll_every_minute'
166+
});
167+
expect(mockOpenWhiskInstance.rules.enable).toHaveBeenCalledWith({
168+
name: 'poll_every_minute'
169+
});
170+
expect(process.exit).toHaveBeenCalledWith(0);
171+
});
172+
173+
it('should timeout if running state never becomes true', async () => {
174+
mockOpenWhiskInstance.actions.invoke.mockResolvedValue({ value: 'false' });
175+
176+
const timeout = 1500; // to simulate in the test env
177+
const mainPromise = mainModule.main(timeout);
178+
jest.advanceTimersByTime(1500);
179+
await mainPromise;
180+
181+
expect(console.error).toHaveBeenCalledWith(
182+
'Timeout: running state did not become true within 30 minutes.'
183+
);
184+
expect(process.exit).toHaveBeenCalledWith(1);
185+
});
186+
});
187+
});

tools/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

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

5+
## `refresh-pdps.js`
6+
7+
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.
8+
The operation happens in the background and the script checks that the cold start was successfully triggered, exiting 0.
9+
10+
### Usage
11+
12+
`-l, --locales <locales>: Comma-separated list of locales (or stores) to delete. For example: en,fr,de`
13+
514
## `check-products-count.js`
615

716
This script checks the product count consistency between the `published-products-index` and the Adobe Commerce store.

tools/refresh-pdps.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
const openwhisk = require('openwhisk');
14+
const { program } = require('commander');
15+
const { exit } = require('process');
16+
17+
require('dotenv').config();
18+
19+
const {
20+
AIO_RUNTIME_NAMESPACE,
21+
AIO_RUNTIME_AUTH,
22+
} = process.env;
23+
24+
if (!AIO_RUNTIME_NAMESPACE || !AIO_RUNTIME_AUTH) {
25+
console.log('Missing required environment variables AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE');
26+
exit(1);
27+
}
28+
29+
const [AIO_STATE_MANAGER_ACTION_NAME, AIO_POLLER_RULE_NAME] = ['state-manager', 'poll_every_minute'];
30+
31+
const ow = openwhisk({
32+
apihost: 'https://adobeioruntime.net',
33+
api_key: AIO_RUNTIME_AUTH,
34+
namespace: AIO_RUNTIME_NAMESPACE,
35+
});
36+
37+
let targetLocales = [];
38+
39+
program
40+
.requiredOption('-l, --locales <locales>', 'Comma-separated list of locales (or stores) to target. For example: en,fr,de')
41+
.parse(process.argv);
42+
43+
const options = program.opts();
44+
45+
if (!options.keys) {
46+
console.error('No locales provided to delete.');
47+
exit(1);
48+
}
49+
50+
targetLocales = options.keys.split(',');
51+
52+
async function checkState(locale) {
53+
const state = await ow.actions.invoke({
54+
name: AIO_STATE_MANAGER_ACTION_NAME,
55+
params: { key: locale, op: 'get' },
56+
blocking: true,
57+
result: true,
58+
});
59+
return state.value;
60+
}
61+
62+
async function flushStoreState(locale) {
63+
await ow.actions.invoke({
64+
name: AIO_STATE_MANAGER_ACTION_NAME,
65+
params: { key: locale, op: 'delete' },
66+
blocking: true,
67+
result: true,
68+
});
69+
}
70+
71+
async function main(timeout = 20 * 60 * 1000) {
72+
73+
74+
75+
try {
76+
// Disable the rule
77+
await ow.rules.disable({ name: AIO_POLLER_RULE_NAME });
78+
79+
// Wait until 'running' is not 'true'
80+
let running = await checkState('running');
81+
while (running === 'true') {
82+
console.log('Waiting for running state to be false...');
83+
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
84+
running = await checkState('running');
85+
}
86+
87+
// Delete specified keys
88+
for (const key of targetLocales) {
89+
await flushStoreState(key);
90+
}
91+
92+
// Re-enable the rule to trigger the poller cycle start
93+
await ow.rules.enable({ name: AIO_POLLER_RULE_NAME });
94+
95+
// Check periodically until 'running' is 'true'
96+
const startTime = Date.now();
97+
while (true) {
98+
running = await checkState('running');
99+
if (running === 'true') {
100+
console.log('Running state is true. Exiting...');
101+
exit(0);
102+
}
103+
if (Date.now() - startTime > timeout) {
104+
console.error('Timeout: running state did not become true within 30 minutes.');
105+
exit(1);
106+
}
107+
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
108+
}
109+
} catch (error) {
110+
console.error('Error:', error);
111+
exit(1);
112+
}
113+
}
114+
115+
module.exports = {
116+
checkState,
117+
flushStoreState,
118+
main,
119+
targetLocales,
120+
};
121+
122+
// Only call main if this file is being run directly
123+
if (require.main === module) {
124+
main();
125+
}

0 commit comments

Comments
 (0)