balancer-observatory-sync.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { describe, expect, it } from 'vitest';
  2. import { syncObservatories } from '@/pages/xray/balancers/balancer-helpers';
  3. import type { XraySettingsValue } from '@/hooks/useXraySetting';
  4. function tpl(routing: Record<string, unknown>, extra: Record<string, unknown> = {}): XraySettingsValue {
  5. return { routing, ...extra } as unknown as XraySettingsValue;
  6. }
  7. // Observatory sections have no reload API in xray-core, so creating one turns
  8. // a balancer save from a live (hot-applied) routing change into a full
  9. // restart. These tests pin the rule: only strategies that genuinely need an
  10. // observer may create one — which, for random/roundRobin, means a fallbackTag
  11. // is set (xray-core then requires the Observatory feature; see #5605).
  12. describe('syncObservatories', () => {
  13. it('does not create burstObservatory for a fresh random balancer (stays hot-appliable)', () => {
  14. const t = tpl({ balancers: [{ tag: 'b1', selector: ['direct'] }] });
  15. syncObservatories(t);
  16. expect(t.burstObservatory).toBeUndefined();
  17. expect(t.observatory).toBeUndefined();
  18. });
  19. it('does not create burstObservatory for roundRobin without fallback', () => {
  20. const t = tpl({ balancers: [{ tag: 'b1', selector: ['a'], strategy: { type: 'roundRobin' } }] });
  21. syncObservatories(t);
  22. expect(t.burstObservatory).toBeUndefined();
  23. });
  24. it('creates burstObservatory for a random balancer with a fallbackTag (#5605)', () => {
  25. const t = tpl({ balancers: [{ tag: 'OverProxy', selector: ['opera-proxy'], fallbackTag: 'warp' }] });
  26. syncObservatories(t);
  27. expect(t.burstObservatory).toBeDefined();
  28. expect((t.burstObservatory as { subjectSelector: string[] }).subjectSelector).toEqual(['opera-proxy']);
  29. });
  30. it('creates burstObservatory for roundRobin with a fallbackTag', () => {
  31. const t = tpl({ balancers: [{ tag: 'b1', selector: ['a'], fallbackTag: 'warp', strategy: { type: 'roundRobin' } }] });
  32. syncObservatories(t);
  33. expect(t.burstObservatory).toBeDefined();
  34. expect((t.burstObservatory as { subjectSelector: string[] }).subjectSelector).toEqual(['a']);
  35. });
  36. it('treats an empty-string fallbackTag as no fallback (stays hot-appliable)', () => {
  37. const t = tpl({ balancers: [{ tag: 'b1', selector: ['a'], fallbackTag: '' }] });
  38. syncObservatories(t);
  39. expect(t.burstObservatory).toBeUndefined();
  40. });
  41. it('removes burstObservatory when a random balancer drops its fallbackTag', () => {
  42. const t = tpl(
  43. { balancers: [{ tag: 'OverProxy', selector: ['opera-proxy'], fallbackTag: '' }] },
  44. { burstObservatory: { subjectSelector: ['opera-proxy'] } },
  45. );
  46. syncObservatories(t);
  47. expect(t.burstObservatory).toBeUndefined();
  48. });
  49. it('removes burstObservatory when a roundRobin balancer drops its fallbackTag', () => {
  50. const t = tpl(
  51. { balancers: [{ tag: 'b1', selector: ['a'], strategy: { type: 'roundRobin' } }] },
  52. { burstObservatory: { subjectSelector: ['a'] } },
  53. );
  54. syncObservatories(t);
  55. expect(t.burstObservatory).toBeUndefined();
  56. });
  57. it('keeps burstObservatory while another fallback balancer still needs it', () => {
  58. const t = tpl(
  59. {
  60. balancers: [
  61. { tag: 'b1', selector: ['a'] },
  62. { tag: 'b2', selector: ['b'], fallbackTag: 'warp', strategy: { type: 'roundRobin' } },
  63. ],
  64. },
  65. { burstObservatory: { subjectSelector: ['a', 'b'] } },
  66. );
  67. syncObservatories(t);
  68. expect(t.burstObservatory).toBeDefined();
  69. expect((t.burstObservatory as { subjectSelector: string[] }).subjectSelector).toEqual(['b', 'a']);
  70. });
  71. it('creates burstObservatory for leastLoad (required by the strategy)', () => {
  72. const t = tpl({ balancers: [{ tag: 'b1', selector: ['a'], strategy: { type: 'leastLoad' } }] });
  73. syncObservatories(t);
  74. expect(t.burstObservatory).toBeDefined();
  75. expect((t.burstObservatory as { subjectSelector: string[] }).subjectSelector).toEqual(['a']);
  76. });
  77. it('creates observatory for leastPing (required by the strategy)', () => {
  78. const t = tpl({ balancers: [{ tag: 'b1', selector: ['a'], strategy: { type: 'leastPing' } }] });
  79. syncObservatories(t);
  80. expect(t.observatory).toBeDefined();
  81. expect((t.observatory as { subjectSelector: string[] }).subjectSelector).toEqual(['a']);
  82. });
  83. it('keeps an existing burstObservatory in sync for random balancers (legacy setups)', () => {
  84. const t = tpl(
  85. { balancers: [{ tag: 'b1', selector: ['a'] }, { tag: 'b2', selector: ['b'], strategy: { type: 'leastLoad' } }] },
  86. { burstObservatory: { subjectSelector: ['stale'] } },
  87. );
  88. syncObservatories(t);
  89. expect((t.burstObservatory as { subjectSelector: string[] }).subjectSelector).toEqual(['b', 'a']);
  90. });
  91. it('removes observatories when no balancer can use them', () => {
  92. const t = tpl({ balancers: [] }, {
  93. observatory: { subjectSelector: ['a'] },
  94. burstObservatory: { subjectSelector: ['a'] },
  95. });
  96. syncObservatories(t);
  97. expect(t.observatory).toBeUndefined();
  98. expect(t.burstObservatory).toBeUndefined();
  99. });
  100. });