Skip to content

Commit 1f10c2f

Browse files
committed
add v3 scenario 12
1 parent f42716c commit 1f10c2f

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
import encoding from 'k6/encoding';
2+
import { check } from 'k6';
3+
import http from 'k6/http';
4+
5+
const baseUrl = __ENV.OPERATOR_URL;
6+
const clientSecret = __ENV.CLIENT_SECRET;
7+
const clientKey = __ENV.CLIENT_KEY;
8+
9+
const generateRPS = 25000;
10+
const refreshRPS = 25000;
11+
12+
// Like scenario 8-v3: 10 concurrent requests per iteration via http.batch().
13+
// Unlike scenario 8-v3: each batch slot gets a freshly generated body with unique DIIs —
14+
// no 45s caching, no shared body across slots.
15+
// identityMapIterationsPerSecond × CONCURRENT_REQUESTS ≈ scenario 2's 1500 RPS
16+
const CONCURRENT_REQUESTS = 10;
17+
const identityMapIterationsPerSecond = 150; // 150 × 10 = 1500 total HTTP requests/s
18+
19+
const warmUpTime = '10m'
20+
const testDuration = '20m'
21+
22+
export const options = {
23+
insecureSkipTLSVerify: true,
24+
noConnectionReuse: false,
25+
scenarios: {
26+
// Warmup scenarios
27+
tokenGenerateWarmup: {
28+
executor: 'ramping-arrival-rate',
29+
exec: 'tokenGenerate',
30+
timeUnit: '1s',
31+
preAllocatedVUs: 200,
32+
maxVUs: 400,
33+
stages: [
34+
{ duration: warmUpTime, target: generateRPS}
35+
],
36+
},
37+
tokenRefreshWarmup: {
38+
executor: 'ramping-arrival-rate',
39+
exec: 'tokenRefresh',
40+
timeUnit: '1s',
41+
preAllocatedVUs: 200,
42+
maxVUs: 400,
43+
stages: [
44+
{ duration: warmUpTime, target: refreshRPS}
45+
],
46+
},
47+
identityMapWarmup: {
48+
executor: 'ramping-arrival-rate',
49+
exec: 'identityMap',
50+
timeUnit: '1s',
51+
preAllocatedVUs: 100,
52+
maxVUs: 200,
53+
stages: [
54+
{ duration: warmUpTime, target: identityMapIterationsPerSecond}
55+
],
56+
},
57+
// Actual testing scenarios
58+
tokenGenerate: {
59+
executor: 'constant-arrival-rate',
60+
exec: 'tokenGenerate',
61+
rate: generateRPS,
62+
timeUnit: '1s',
63+
preAllocatedVUs: 200,
64+
maxVUs: 400,
65+
duration: testDuration,
66+
gracefulStop: '0s',
67+
startTime: warmUpTime,
68+
},
69+
tokenRefresh: {
70+
executor: 'constant-arrival-rate',
71+
exec: 'tokenRefresh',
72+
rate: refreshRPS,
73+
timeUnit: '1s',
74+
preAllocatedVUs: 200,
75+
maxVUs: 400,
76+
duration: testDuration,
77+
gracefulStop: '0s',
78+
startTime: warmUpTime,
79+
},
80+
identityMap: {
81+
executor: 'constant-arrival-rate',
82+
exec: 'identityMap',
83+
rate: identityMapIterationsPerSecond,
84+
timeUnit: '1s',
85+
preAllocatedVUs: 100,
86+
maxVUs: 200,
87+
duration: testDuration,
88+
gracefulStop: '0s',
89+
startTime: warmUpTime,
90+
},
91+
},
92+
// So we get count in the summary, to demonstrate different metrics are different
93+
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'],
94+
thresholds: {
95+
// Intentionally empty. We'll programatically define our bogus
96+
// thresholds (to generate the sub-metrics) below. In your real-world
97+
// load test, you can add any real threshoulds you want here.
98+
}
99+
};
100+
101+
// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3
102+
for (let key in options.scenarios) {
103+
// Each scenario automaticall tags the metrics it generates with its own name
104+
let thresholdName = `http_req_duration{scenario:${key}}`;
105+
// Check to prevent us from overwriting a threshold that already exists
106+
if (!options.thresholds[thresholdName]) {
107+
options.thresholds[thresholdName] = [];
108+
}
109+
// 'max>=0' is a bogus condition that will always be fulfilled
110+
options.thresholds[thresholdName].push('max>=0');
111+
}
112+
113+
export async function setup() {
114+
var token = await generateRefreshRequest();
115+
return {
116+
tokenGenerate: null,
117+
refreshToken: token
118+
};
119+
120+
async function generateRefreshRequest() {
121+
let randomSuffix = Math.floor(Math.random() * 1_000_000_001);
122+
let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`});
123+
var requestData = {
124+
endpoint: '/v2/token/generate',
125+
requestBody: request,
126+
}
127+
let response = await send(requestData, clientKey);
128+
let decrypt = await decryptEnvelope(response.body, clientSecret)
129+
return decrypt.body.refresh_token;
130+
};
131+
}
132+
133+
export function handleSummary(data) {
134+
return {
135+
'summary.json': JSON.stringify(data),
136+
}
137+
}
138+
139+
// Scenarios
140+
export async function tokenGenerate(data) {
141+
const endpoint = '/v2/token/generate';
142+
if (data.tokenGenerate == null) {
143+
var newData = await generateTokenGenerateRequestWithTime();
144+
data.tokenGenerate = newData;
145+
} else if (data.tokenGenerate.time < (Date.now() - 45000)) {
146+
data.tokenGenerate = await generateTokenGenerateRequestWithTime();
147+
}
148+
149+
var requestBody = data.tokenGenerate.requestBody;
150+
var tokenGenerateData = {
151+
endpoint: endpoint,
152+
requestBody: requestBody,
153+
}
154+
155+
execute(tokenGenerateData, true);
156+
}
157+
158+
export function tokenRefresh(data) {
159+
var requestBody = data.refreshToken;
160+
var refreshData = {
161+
endpoint: '/v2/token/refresh',
162+
requestBody: requestBody
163+
}
164+
165+
execute(refreshData, false);
166+
}
167+
168+
export async function identityMap() {
169+
const endpoint = '/v3/identity/map';
170+
const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } };
171+
172+
// Generate a separate encrypted request body with unique DIIs for each
173+
// concurrent request — no caching, no shared bodies between batch slots.
174+
const batchRequests = [];
175+
for (let i = 0; i < CONCURRENT_REQUESTS; i++) {
176+
const requestData = await generateIdentityMapRequestWithTime(5000);
177+
batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]);
178+
}
179+
180+
const responses = http.batch(batchRequests);
181+
for (const r of responses) {
182+
check(r, { 'status is 200': res => res.status === 200 });
183+
}
184+
}
185+
186+
// Helpers
187+
async function createReqWithTimestamp(timestampArr, obj) {
188+
var envelope = getEnvelopeWithTimestamp(timestampArr, obj);
189+
return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer);
190+
}
191+
192+
function generateIdentityMapRequest(emailCount) {
193+
var data = {
194+
'email': []
195+
};
196+
197+
let randomSuffix = Math.floor(Math.random() * 1_000_000_001);
198+
for (var i = 0; i < emailCount; ++i) {
199+
data.email.push(`test${randomSuffix}${i}@example.com`);
200+
}
201+
202+
return data;
203+
}
204+
205+
function send(data, auth) {
206+
var options = {};
207+
if (auth) {
208+
options.headers = {
209+
'Authorization': `Bearer ${clientKey}`
210+
};
211+
}
212+
213+
return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options);
214+
}
215+
216+
function execute(data, auth) {
217+
var response = send(data, auth);
218+
219+
check(response, {
220+
'status is 200': r => r.status === 200,
221+
});
222+
}
223+
224+
async function encryptEnvelope(envelope, clientSecret) {
225+
const rawKey = encoding.b64decode(clientSecret);
226+
const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [
227+
"encrypt",
228+
"decrypt",
229+
]);
230+
231+
const iv = crypto.getRandomValues(new Uint8Array(12));
232+
233+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
234+
{
235+
name: "AES-GCM",
236+
iv: iv,
237+
},
238+
key,
239+
envelope
240+
));
241+
242+
const result = new Uint8Array(+(1 + iv.length + ciphertext.length));
243+
244+
// The version of the envelope format.
245+
result[0] = 1;
246+
247+
result.set(iv, 1);
248+
249+
// The tag is at the end of ciphertext.
250+
result.set(ciphertext, 1 + iv.length);
251+
252+
return result;
253+
}
254+
255+
async function decryptEnvelope(envelope, clientSecret) {
256+
const rawKey = encoding.b64decode(clientSecret);
257+
const rawData = encoding.b64decode(envelope);
258+
const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [
259+
"encrypt",
260+
"decrypt",
261+
]);
262+
const length = rawData.byteLength;
263+
const iv = rawData.slice(0, 12);
264+
265+
const decrypted = await crypto.subtle.decrypt(
266+
{
267+
name: "AES-GCM",
268+
iv: iv,
269+
tagLength: 128
270+
},
271+
key,
272+
rawData.slice(12)
273+
);
274+
275+
276+
const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16)));
277+
const response = JSON.parse(decryptedResponse);
278+
279+
return response;
280+
}
281+
282+
function getEnvelopeWithTimestamp(timestampArray, obj) {
283+
var randomBytes = new Uint8Array(8);
284+
crypto.getRandomValues(randomBytes);
285+
286+
var payload = stringToUint8Array(JSON.stringify(obj));
287+
288+
var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length);
289+
envelope.set(timestampArray);
290+
envelope.set(randomBytes, timestampArray.length);
291+
envelope.set(payload, timestampArray.length + randomBytes.length);
292+
293+
return envelope;
294+
295+
}
296+
function getEnvelope(obj) {
297+
var timestampArr = new Uint8Array(getTimestamp());
298+
return getEnvelopeWithTimestamp(timestampArr, obj);
299+
}
300+
301+
function getTimestamp() {
302+
const now = Date.now();
303+
return getTimestampFromTime(now);
304+
}
305+
306+
function getTimestampFromTime(time) {
307+
const res = new ArrayBuffer(8);
308+
const { hi, lo } = Get32BitPartsBE(time);
309+
const view = new DataView(res);
310+
view.setUint32(0, hi, false);
311+
view.setUint32(4, lo, false);
312+
return res;
313+
}
314+
315+
// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html
316+
function Get32BitPartsBE(bigNumber) {
317+
if (bigNumber > 9007199254740991) {
318+
// Max int that JavaScript can represent is 2^53.
319+
throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber);
320+
}
321+
322+
var bigNumberAsBinaryStr = bigNumber.toString(2);
323+
// Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left
324+
var bigNumberAsBinaryStr2 = '';
325+
for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) {
326+
bigNumberAsBinaryStr2 += '0';
327+
};
328+
329+
bigNumberAsBinaryStr2 += bigNumberAsBinaryStr;
330+
331+
return {
332+
hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2),
333+
lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2),
334+
};
335+
}
336+
337+
function stringToUint8Array(str) {
338+
const buffer = new ArrayBuffer(str.length);
339+
const view = new Uint8Array(buffer);
340+
for (var i = 0; i < str.length; i++) {
341+
view[i] = str.charCodeAt(i);
342+
}
343+
return view;
344+
}
345+
346+
async function createReq(obj) {
347+
var envelope = getEnvelope(obj);
348+
return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer);
349+
};
350+
351+
async function generateRequestWithTime(obj) {
352+
var time = Date.now();
353+
var timestampArr = new Uint8Array(getTimestampFromTime(time));
354+
var requestBody = await createReqWithTimestamp(timestampArr, obj);
355+
var element = {
356+
time: time,
357+
requestBody: requestBody
358+
};
359+
360+
return element;
361+
}
362+
363+
async function generateTokenGenerateRequestWithTime() {
364+
let randomSuffix = Math.floor(Math.random() * 1_000_000_001);
365+
let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` };
366+
return await generateRequestWithTime(requestData);
367+
}
368+
369+
async function generateIdentityMapRequestWithTime(emailCount) {
370+
let data = generateIdentityMapRequest(emailCount);
371+
return await generateRequestWithTime(data);
372+
}
373+
374+
const generateSinceTimestampStr = () => {
375+
var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */);
376+
var year = date.getFullYear();
377+
var month = (date.getMonth() + 1).toString().padStart(2, '0');
378+
var day = date.getDate().toString().padStart(2, '0');
379+
380+
return `${year}-${month}-${day}T00:00:00`;
381+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
COMMENT=$1
4+
5+
if [ "$#" -ne 1 ]; then
6+
COMMENT=$( date '+%F_%H:%M:%S' )
7+
fi
8+
9+
./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-12-v3.js $COMMENT

0 commit comments

Comments
 (0)