Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Johndere Expense Portal

React SPA, Express API, optional Docker/nginx. Load tests live in load-tests/.

(load-tests/testscript-expense-auth.js) — two scenarios

Side-by-side terminal captures from the commented vs active script blocks in the same file.

Commented — login every iteration
k6: login each iteration, threshold fail
  • Each iteration: POST /api/v1/auth/login then GET /api/v1/auth/me.
  • Stresses auth (bcrypt, JWT, DB) every loop.
  • Checks can stay green while an http_req_duration p(95) SLO (e.g. 800 ms) fails.
Active — login once per VU
k6: reuse token cookie per VU
  • Per-VU token from Set-Cookie; later calls use Cookie: token=….
  • Closer to a real browser session (sign in once).
  • Fewer logins → lower tail latency; thresholds pass.

Server: login sets httpOnly tokenserver/controllers/auth.js.

k6 run -e BASE_URL=http://localhost -e TEST_EMAIL=... -e TEST_PASSWORD=... load-tests/testscript-expense-auth.js

(load-tests/testscript-expense-create.js) — two scenarios

Commented — blocking email + sequential DB reads
k6: blocking email, threshold fail
  • Each request: POST /api/v1/expenses blocks until both sendEmail() calls resolve.
  • Sequential DB reads stall the request chain further.
  • Result: ~10s latency spikes, 29% failure rate, thresholds fail.
  • No async offloading — every operation blocks inside the request lifecycle until fully resolved.
// slow — blocks response until emails finish
await sendEmail(employeePayload);
await sendEmail(managerPayload);
res.status(201).json({ message: "Expense submitted", expense });

// sequential DB reads
const employee = await User.findById(req.user.userId);
const manager = await User.findOne({ role: "manager" });

//  nothing escapes the request — user waits for all of it
// slow — blocks response until emails finish
const expense  = await Expense.create(data);
const employee = await User.findById(req.user.userId);
const manager  = await User.findOne({ role: "manager" });
await sendEmail(employeePayload);   // still waiting...
await sendEmail(managerPayload);    // still waiting...
res.status(201).json({ ... });      // user finally gets a response
Active — fire-and-forget email + parallel DB reads
k6: async email, thresholds pass
  • Response sent immediately after DB write; emails offloaded via setImmediate.
  • DB reads parallelised with Promise.all.
  • Result: latency drops sharply, 0% failures, all thresholds pass.
  • Emails wrapped in setImmediate with full async error handling — completely outside the request lifecycle.
//  respond first, email in background
//  fire-and-forget with proper async/error handling
res.status(201).json({ message: "Expense submitted", expense });
setImmediate(() => {
  sendEmail(employeePayload);
  sendEmail(managerPayload);
});

//  parallel DB reads
const [employee, manager] = await Promise.all([
  User.findById(req.user.userId).select("name email"),
  User.findOne({ role: "manager" }).select("name email"),
]);

Server: createExpense lives in server/controllers/expense.js.

k6 run -e BASE_URL=http://localhost -e TEST_EMAIL=... -e TEST_PASSWORD=... load-tests/testscript-expense-create.js

(load-tests/testscript-expense-approve.js) — two scenarios

Commented — sequential DB reads + race condition
k6: approval, race condition fail
  • Two separate DB operations create a race window — another request can modify the expense between reads.
  • Sequential DB reads stall the request chain.
  • Blocking email I/O inside the request lifecycle adds tail latency.
  • Result: 0.33% failure rate, 10s max latency spike, approve success or race check fails.
//  two round trips — race window between them
const expense = await Expense.findById(id);
if (expense.status !== "pending") {
  throw new BadRequestError(...);
}
const updatedExpense = await Expense.findByIdAndUpdate(
  id,
  { status },
  { new: true }
);
Active — atomic DB operation, no race
k6: approval, thresholds pass
  • Single atomic findOneAndUpdate with condition — no race window possible.
  • Eliminates the ~0.33% concurrent approval failures seen under load.
  • Removes long tail latency spikes; p(95) drops from 376ms to 169ms.
  • Result: 0.00% failures, 100% checks passed, all thresholds pass.
// atomic — condition + update in one DB round trip
const updatedExpense = await Expense.findOneAndUpdate(
  { _id: id, status: "pending" }, // atomic condition
  { $set: { status } },
  { new: true },
);
if (!updatedExpense) {
  throw new BadRequestError("Expense already processed or not found");
}

Server: approveExpense lives in server/controllers/expense.js.

k6 run -e BASE_URL=http://localhost -e TEST_EMAIL=... -e TEST_PASSWORD=... load-tests/testscript-expense-approve.js

About

John Deere Expense ecosystem: Prototype v1

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages