cooperative-yield.test.ts 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. /**
  2. * Cooperative-yield helper + the async contract of the main-thread resolution
  3. * spans it protects (#1091).
  4. *
  5. * Background: reference resolution and callback-edge synthesis run on the
  6. * indexer's MAIN thread. The #850 liveness watchdog SIGKILLs the process when
  7. * that thread doesn't turn its event loop within the timeout window, because its
  8. * heartbeat is a timer on that same thread. On a large repo those spans run for
  9. * minutes, so they must yield periodically or a VALID index gets killed. These
  10. * tests pin (a) the yielder's budget semantics and (b) that the three long spans
  11. * stayed `async` so they CAN yield — a revert to a synchronous version would
  12. * reintroduce the wedge, and the AsyncFunction assertions fail loudly if so.
  13. */
  14. import { describe, it, expect } from 'vitest';
  15. import { createYielder, DEFAULT_YIELD_BUDGET_MS } from '../src/resolution/cooperative-yield';
  16. import { synthesizeCallbackEdges } from '../src/resolution/callback-synthesizer';
  17. import { ReferenceResolver } from '../src/resolution/index';
  18. /**
  19. * A `setImmediate` callback runs in the check phase — AFTER the microtask queue
  20. * drains. So if `await maybeYield()` did NOT cross a macrotask boundary (it was
  21. * under budget and returned a synchronously-resolved promise), a `setImmediate`
  22. * scheduled just before it has NOT fired yet. If it DID yield (awaited its own
  23. * `setImmediate`), the earlier `setImmediate` — queued first, FIFO — has fired.
  24. * This makes "did it yield?" a deterministic, non-timing assertion.
  25. */
  26. async function yieldedDuring(maybeYield: () => Promise<void>): Promise<boolean> {
  27. let macrotaskRan = false;
  28. setImmediate(() => { macrotaskRan = true; });
  29. await maybeYield();
  30. return macrotaskRan;
  31. }
  32. describe('createYielder', () => {
  33. it('does not yield while under the time budget', async () => {
  34. const maybeYield = createYielder(100_000); // effectively never elapses in-test
  35. expect(await yieldedDuring(maybeYield)).toBe(false);
  36. // Repeated calls stay coalesced — still no macrotask boundary crossed.
  37. expect(await yieldedDuring(maybeYield)).toBe(false);
  38. });
  39. it('yields once the budget has elapsed, then resets', async () => {
  40. const maybeYield = createYielder(0); // 0ms budget → every checkpoint yields
  41. expect(await yieldedDuring(maybeYield)).toBe(true);
  42. // Reset: the next checkpoint also yields (budget is measured from the last
  43. // yield, and 0ms has "elapsed" again).
  44. expect(await yieldedDuring(maybeYield)).toBe(true);
  45. });
  46. it('yields after real wall-clock exceeds the budget', async () => {
  47. const maybeYield = createYielder(20);
  48. expect(await yieldedDuring(maybeYield)).toBe(false); // fresh — under budget
  49. const until = Date.now() + 35;
  50. while (Date.now() < until) { /* busy-wait past the 20ms budget */ }
  51. expect(await yieldedDuring(maybeYield)).toBe(true);
  52. });
  53. it('exposes a sane default budget under the watchdog heartbeat cadence', () => {
  54. // The watchdog writes a heartbeat every ~1s at minimum; the yield budget
  55. // must be well under that so a beat can always land between yields.
  56. expect(DEFAULT_YIELD_BUDGET_MS).toBeGreaterThan(0);
  57. expect(DEFAULT_YIELD_BUDGET_MS).toBeLessThan(1000);
  58. });
  59. });
  60. describe('main-thread resolution spans stay async (so they can yield) — #1091', () => {
  61. it('synthesizeCallbackEdges is an async function', () => {
  62. expect(synthesizeCallbackEdges.constructor.name).toBe('AsyncFunction');
  63. });
  64. it('resolveChainedCallsViaConformance is an async function', () => {
  65. expect(ReferenceResolver.prototype.resolveChainedCallsViaConformance.constructor.name).toBe('AsyncFunction');
  66. });
  67. it('resolveDeferredThisMemberRefs is an async function', () => {
  68. expect(ReferenceResolver.prototype.resolveDeferredThisMemberRefs.constructor.name).toBe('AsyncFunction');
  69. });
  70. it('resolveAndPersistBatched is an async function', () => {
  71. expect(ReferenceResolver.prototype.resolveAndPersistBatched.constructor.name).toBe('AsyncFunction');
  72. });
  73. });