64b26cd54c15426a61f857fe068f7466d11c814d
[poolifier.git] / tests / pools / selection-strategies / selection-strategies.test.js
1 const { expect } = require('expect')
2 const {
3 WorkerChoiceStrategies,
4 DynamicThreadPool,
5 FixedThreadPool
6 } = require('../../../lib/index')
7
8 describe('Selection strategies test suite', () => {
9 const min = 0
10 const max = 3
11
12 it('Verify that WorkerChoiceStrategies enumeration provides string values', () => {
13 expect(WorkerChoiceStrategies.ROUND_ROBIN).toBe('ROUND_ROBIN')
14 expect(WorkerChoiceStrategies.LESS_RECENTLY_USED).toBe('LESS_RECENTLY_USED')
15 expect(WorkerChoiceStrategies.FAIR_SHARE).toBe('FAIR_SHARE')
16 expect(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN).toBe(
17 'WEIGHTED_ROUND_ROBIN'
18 )
19 })
20
21 it('Verify ROUND_ROBIN strategy is the default at pool creation', async () => {
22 const pool = new DynamicThreadPool(
23 min,
24 max,
25 './tests/worker-files/thread/testWorker.js'
26 )
27 expect(pool.opts.workerChoiceStrategy).toBe(
28 WorkerChoiceStrategies.ROUND_ROBIN
29 )
30 // We need to clean up the resources after our test
31 await pool.destroy()
32 })
33
34 it('Verify ROUND_ROBIN strategy is taken at pool creation', async () => {
35 const pool = new FixedThreadPool(
36 max,
37 './tests/worker-files/thread/testWorker.js',
38 { workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN }
39 )
40 expect(pool.opts.workerChoiceStrategy).toBe(
41 WorkerChoiceStrategies.ROUND_ROBIN
42 )
43 expect(
44 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy().nextWorkerIndex
45 ).toBe(0)
46 // We need to clean up the resources after our test
47 await pool.destroy()
48 })
49
50 it('Verify ROUND_ROBIN strategy can be set after pool creation', async () => {
51 const pool = new DynamicThreadPool(
52 min,
53 max,
54 './tests/worker-files/thread/testWorker.js'
55 )
56 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.ROUND_ROBIN)
57 expect(pool.opts.workerChoiceStrategy).toBe(
58 WorkerChoiceStrategies.ROUND_ROBIN
59 )
60 // We need to clean up the resources after our test
61 await pool.destroy()
62 })
63
64 it('Verify ROUND_ROBIN strategy default tasks usage statistics requirements', async () => {
65 let pool = new FixedThreadPool(
66 max,
67 './tests/worker-files/thread/testWorker.js'
68 )
69 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.ROUND_ROBIN)
70 expect(
71 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
72 .requiredStatistics.runTime
73 ).toBe(false)
74 await pool.destroy()
75 pool = new DynamicThreadPool(
76 min,
77 max,
78 './tests/worker-files/thread/testWorker.js'
79 )
80 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.ROUND_ROBIN)
81 expect(
82 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
83 .requiredStatistics.runTime
84 ).toBe(false)
85 // We need to clean up the resources after our test
86 await pool.destroy()
87 })
88
89 it('Verify ROUND_ROBIN strategy can be run in a fixed pool', async () => {
90 const pool = new FixedThreadPool(
91 max,
92 './tests/worker-files/thread/testWorker.js',
93 { workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN }
94 )
95 expect(pool.opts.workerChoiceStrategy).toBe(
96 WorkerChoiceStrategies.ROUND_ROBIN
97 )
98 // TODO: Create a better test to cover `RoundRobinWorkerChoiceStrategy#choose`
99 const promises = []
100 for (let i = 0; i < max * 2; i++) {
101 promises.push(pool.execute())
102 }
103 await Promise.all(promises)
104 // We need to clean up the resources after our test
105 await pool.destroy()
106 })
107
108 it('Verify ROUND_ROBIN strategy can be run in a dynamic pool', async () => {
109 const pool = new DynamicThreadPool(
110 min,
111 max,
112 './tests/worker-files/thread/testWorker.js',
113 { workerChoiceStrategy: WorkerChoiceStrategies.ROUND_ROBIN }
114 )
115 expect(pool.opts.workerChoiceStrategy).toBe(
116 WorkerChoiceStrategies.ROUND_ROBIN
117 )
118 // TODO: Create a better test to cover `RoundRobinWorkerChoiceStrategy#choose`
119 const promises = []
120 for (let i = 0; i < max * 2; i++) {
121 promises.push(pool.execute())
122 }
123 await Promise.all(promises)
124 // We need to clean up the resources after our test
125 await pool.destroy()
126 })
127
128 it('Verify ROUND_ROBIN strategy internals are resets after setting it', async () => {
129 let pool = new FixedThreadPool(
130 max,
131 './tests/worker-files/thread/testWorker.js',
132 { workerChoiceStrategy: WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN }
133 )
134 expect(
135 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy().nextWorkerIndex
136 ).toBeUndefined()
137 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.ROUND_ROBIN)
138 expect(
139 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy().nextWorkerIndex
140 ).toBe(0)
141 await pool.destroy()
142 pool = new DynamicThreadPool(
143 min,
144 max,
145 './tests/worker-files/thread/testWorker.js',
146 { workerChoiceStrategy: WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN }
147 )
148 expect(
149 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
150 .workerChoiceStrategy.nextWorkerIndex
151 ).toBeUndefined()
152 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.ROUND_ROBIN)
153 expect(
154 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
155 .workerChoiceStrategy.nextWorkerIndex
156 ).toBe(0)
157 // We need to clean up the resources after our test
158 await pool.destroy()
159 })
160
161 it('Verify LESS_RECENTLY_USED strategy is taken at pool creation', async () => {
162 const pool = new FixedThreadPool(
163 max,
164 './tests/worker-files/thread/testWorker.js',
165 { workerChoiceStrategy: WorkerChoiceStrategies.LESS_RECENTLY_USED }
166 )
167 expect(pool.opts.workerChoiceStrategy).toBe(
168 WorkerChoiceStrategies.LESS_RECENTLY_USED
169 )
170 // We need to clean up the resources after our test
171 await pool.destroy()
172 })
173
174 it('Verify LESS_RECENTLY_USED strategy can be set after pool creation', async () => {
175 const pool = new FixedThreadPool(
176 max,
177 './tests/worker-files/thread/testWorker.js'
178 )
179 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.LESS_RECENTLY_USED)
180 expect(pool.opts.workerChoiceStrategy).toBe(
181 WorkerChoiceStrategies.LESS_RECENTLY_USED
182 )
183 // We need to clean up the resources after our test
184 await pool.destroy()
185 })
186
187 it('Verify LESS_RECENTLY_USED strategy default tasks usage statistics requirements', async () => {
188 let pool = new FixedThreadPool(
189 max,
190 './tests/worker-files/thread/testWorker.js'
191 )
192 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.LESS_RECENTLY_USED)
193 expect(
194 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
195 .requiredStatistics.runTime
196 ).toBe(false)
197 await pool.destroy()
198 pool = new DynamicThreadPool(
199 min,
200 max,
201 './tests/worker-files/thread/testWorker.js'
202 )
203 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.LESS_RECENTLY_USED)
204 expect(
205 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
206 .requiredStatistics.runTime
207 ).toBe(false)
208 // We need to clean up the resources after our test
209 await pool.destroy()
210 })
211
212 it('Verify LESS_RECENTLY_USED strategy can be run in a fixed pool', async () => {
213 const pool = new FixedThreadPool(
214 max,
215 './tests/worker-files/thread/testWorker.js',
216 { workerChoiceStrategy: WorkerChoiceStrategies.LESS_RECENTLY_USED }
217 )
218 // TODO: Create a better test to cover `LessRecentlyUsedWorkerChoiceStrategy#choose`
219 const promises = []
220 for (let i = 0; i < max * 2; i++) {
221 promises.push(pool.execute())
222 }
223 await Promise.all(promises)
224 // We need to clean up the resources after our test
225 await pool.destroy()
226 })
227
228 it('Verify LESS_RECENTLY_USED strategy can be run in a dynamic pool', async () => {
229 const pool = new DynamicThreadPool(
230 min,
231 max,
232 './tests/worker-files/thread/testWorker.js',
233 { workerChoiceStrategy: WorkerChoiceStrategies.LESS_RECENTLY_USED }
234 )
235 // TODO: Create a better test to cover `LessRecentlyUsedWorkerChoiceStrategy#choose`
236 const promises = []
237 for (let i = 0; i < max * 2; i++) {
238 promises.push(pool.execute())
239 }
240 await Promise.all(promises)
241 // We need to clean up the resources after our test
242 await pool.destroy()
243 })
244
245 it('Verify FAIR_SHARE strategy is taken at pool creation', async () => {
246 const pool = new FixedThreadPool(
247 max,
248 './tests/worker-files/thread/testWorker.js',
249 { workerChoiceStrategy: WorkerChoiceStrategies.FAIR_SHARE }
250 )
251 expect(pool.opts.workerChoiceStrategy).toBe(
252 WorkerChoiceStrategies.FAIR_SHARE
253 )
254 for (const worker of pool.workerChoiceStrategyContext
255 .getWorkerChoiceStrategy()
256 .workerLastVirtualTaskTimestamp.keys()) {
257 expect(
258 pool.workerChoiceStrategyContext
259 .getWorkerChoiceStrategy()
260 .workerLastVirtualTaskTimestamp.get(worker).start
261 ).toBe(0)
262 expect(
263 pool.workerChoiceStrategyContext
264 .getWorkerChoiceStrategy()
265 .workerLastVirtualTaskTimestamp.get(worker).end
266 ).toBe(0)
267 }
268 // We need to clean up the resources after our test
269 await pool.destroy()
270 })
271
272 it('Verify FAIR_SHARE strategy can be set after pool creation', async () => {
273 const pool = new FixedThreadPool(
274 max,
275 './tests/worker-files/thread/testWorker.js'
276 )
277 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
278 expect(pool.opts.workerChoiceStrategy).toBe(
279 WorkerChoiceStrategies.FAIR_SHARE
280 )
281 // We need to clean up the resources after our test
282 await pool.destroy()
283 })
284
285 it('Verify FAIR_SHARE strategy default tasks usage statistics requirements', async () => {
286 let pool = new FixedThreadPool(
287 max,
288 './tests/worker-files/thread/testWorker.js'
289 )
290 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
291 expect(
292 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
293 .requiredStatistics.runTime
294 ).toBe(true)
295 await pool.destroy()
296 pool = new DynamicThreadPool(
297 min,
298 max,
299 './tests/worker-files/thread/testWorker.js'
300 )
301 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
302 expect(
303 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
304 .requiredStatistics.runTime
305 ).toBe(true)
306 // We need to clean up the resources after our test
307 await pool.destroy()
308 })
309
310 it('Verify FAIR_SHARE strategy can be run in a fixed pool', async () => {
311 const pool = new FixedThreadPool(
312 max,
313 './tests/worker-files/thread/testWorker.js',
314 { workerChoiceStrategy: WorkerChoiceStrategies.FAIR_SHARE }
315 )
316 // TODO: Create a better test to cover `FairShareChoiceStrategy#choose`
317 const promises = []
318 for (let i = 0; i < max * 2; i++) {
319 promises.push(pool.execute())
320 }
321 await Promise.all(promises)
322 // We need to clean up the resources after our test
323 await pool.destroy()
324 })
325
326 it('Verify FAIR_SHARE strategy can be run in a dynamic pool', async () => {
327 const pool = new DynamicThreadPool(
328 min,
329 max,
330 './tests/worker-files/thread/testWorker.js',
331 { workerChoiceStrategy: WorkerChoiceStrategies.FAIR_SHARE }
332 )
333 // TODO: Create a better test to cover `FairShareChoiceStrategy#choose`
334 const promises = []
335 for (let i = 0; i < max * 2; i++) {
336 promises.push(pool.execute())
337 }
338 await Promise.all(promises)
339 // We need to clean up the resources after our test
340 await pool.destroy()
341 })
342
343 it('Verify FAIR_SHARE strategy internals are resets after setting it', async () => {
344 let pool = new FixedThreadPool(
345 max,
346 './tests/worker-files/thread/testWorker.js'
347 )
348 expect(
349 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
350 .workerLastVirtualTaskTimestamp
351 ).toBeUndefined()
352 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
353 for (const worker of pool.workerChoiceStrategyContext
354 .getWorkerChoiceStrategy()
355 .workerLastVirtualTaskTimestamp.keys()) {
356 expect(
357 pool.workerChoiceStrategyContext
358 .getWorkerChoiceStrategy()
359 .workerLastVirtualTaskTimestamp.get(worker).start
360 ).toBe(0)
361 expect(
362 pool.workerChoiceStrategyContext
363 .getWorkerChoiceStrategy()
364 .workerLastVirtualTaskTimestamp.get(worker).end
365 ).toBe(0)
366 }
367 await pool.destroy()
368 pool = new DynamicThreadPool(
369 min,
370 max,
371 './tests/worker-files/thread/testWorker.js'
372 )
373 expect(
374 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
375 .workerChoiceStrategy.workerLastVirtualTaskTimestamp
376 ).toBeUndefined()
377 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.FAIR_SHARE)
378 for (const worker of pool.workerChoiceStrategyContext
379 .getWorkerChoiceStrategy()
380 .workerChoiceStrategy.workerLastVirtualTaskTimestamp.keys()) {
381 expect(
382 pool.workerChoiceStrategyContext
383 .getWorkerChoiceStrategy()
384 .workerChoiceStrategy.workerLastVirtualTaskTimestamp.get(worker).start
385 ).toBe(0)
386 expect(
387 pool.workerChoiceStrategyContext
388 .getWorkerChoiceStrategy()
389 .workerChoiceStrategy.workerLastVirtualTaskTimestamp.get(worker).end
390 ).toBe(0)
391 }
392 // We need to clean up the resources after our test
393 await pool.destroy()
394 })
395
396 it('Verify WEIGHTED_ROUND_ROBIN strategy is taken at pool creation', async () => {
397 const pool = new FixedThreadPool(
398 max,
399 './tests/worker-files/thread/testWorker.js',
400 { workerChoiceStrategy: WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN }
401 )
402 expect(pool.opts.workerChoiceStrategy).toBe(
403 WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN
404 )
405 expect(
406 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
407 .previousWorkerIndex
408 ).toBe(0)
409 expect(
410 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
411 .currentWorkerIndex
412 ).toBe(0)
413 expect(
414 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
415 .defaultWorkerWeight
416 ).toBeGreaterThan(0)
417 for (const worker of pool.workerChoiceStrategyContext
418 .getWorkerChoiceStrategy()
419 .workersTaskRunTime.keys()) {
420 expect(
421 pool.workerChoiceStrategyContext
422 .getWorkerChoiceStrategy()
423 .workersTaskRunTime.get(worker).weight
424 ).toBeGreaterThan(0)
425 expect(
426 pool.workerChoiceStrategyContext
427 .getWorkerChoiceStrategy()
428 .workersTaskRunTime.get(worker).runTime
429 ).toBe(0)
430 }
431 // We need to clean up the resources after our test
432 await pool.destroy()
433 })
434
435 it('Verify WEIGHTED_ROUND_ROBIN strategy can be set after pool creation', async () => {
436 const pool = new FixedThreadPool(
437 max,
438 './tests/worker-files/thread/testWorker.js'
439 )
440 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN)
441 expect(pool.opts.workerChoiceStrategy).toBe(
442 WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN
443 )
444 // We need to clean up the resources after our test
445 await pool.destroy()
446 })
447
448 it('Verify WEIGHTED_ROUND_ROBIN strategy default tasks usage statistics requirements', async () => {
449 let pool = new FixedThreadPool(
450 max,
451 './tests/worker-files/thread/testWorker.js'
452 )
453 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN)
454 expect(
455 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
456 .requiredStatistics.runTime
457 ).toBe(true)
458 await pool.destroy()
459 pool = new DynamicThreadPool(
460 min,
461 max,
462 './tests/worker-files/thread/testWorker.js'
463 )
464 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN)
465 expect(
466 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
467 .requiredStatistics.runTime
468 ).toBe(true)
469 // We need to clean up the resources after our test
470 await pool.destroy()
471 })
472
473 it('Verify WEIGHTED_ROUND_ROBIN strategy can be run in a fixed pool', async () => {
474 const pool = new FixedThreadPool(
475 max,
476 './tests/worker-files/thread/testWorker.js',
477 { workerChoiceStrategy: WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN }
478 )
479 // TODO: Create a better test to cover `WeightedRoundRobinWorkerChoiceStrategy#choose`
480 const promises = []
481 for (let i = 0; i < max * 2; i++) {
482 promises.push(pool.execute())
483 }
484 await Promise.all(promises)
485 // We need to clean up the resources after our test
486 await pool.destroy()
487 })
488
489 it('Verify WEIGHTED_ROUND_ROBIN strategy can be run in a dynamic pool', async () => {
490 const pool = new DynamicThreadPool(
491 min,
492 max,
493 './tests/worker-files/thread/testWorker.js',
494 { workerChoiceStrategy: WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN }
495 )
496 // TODO: Create a better test to cover `WeightedRoundRobinWorkerChoiceStrategy#choose`
497 const promises = []
498 for (let i = 0; i < max * 2; i++) {
499 promises.push(pool.execute())
500 }
501 await Promise.all(promises)
502 // We need to clean up the resources after our test
503 await pool.destroy()
504 })
505
506 it('Verify WEIGHTED_ROUND_ROBIN strategy internals are resets after setting it', async () => {
507 let pool = new FixedThreadPool(
508 max,
509 './tests/worker-files/thread/testWorker.js'
510 )
511 expect(
512 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
513 .previousWorkerIndex
514 ).toBeUndefined()
515 expect(
516 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
517 .currentWorkerIndex
518 ).toBeUndefined()
519 expect(
520 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
521 .defaultWorkerWeight
522 ).toBeUndefined()
523 expect(
524 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
525 .workersTaskRunTime
526 ).toBeUndefined()
527 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN)
528 expect(
529 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
530 .previousWorkerIndex
531 ).toBe(0)
532 expect(
533 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
534 .currentWorkerIndex
535 ).toBe(0)
536 expect(
537 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
538 .defaultWorkerWeight
539 ).toBeGreaterThan(0)
540 for (const worker of pool.workerChoiceStrategyContext
541 .getWorkerChoiceStrategy()
542 .workersTaskRunTime.keys()) {
543 expect(
544 pool.workerChoiceStrategyContext
545 .getWorkerChoiceStrategy()
546 .workersTaskRunTime.get(worker).runTime
547 ).toBe(0)
548 }
549 await pool.destroy()
550 pool = new DynamicThreadPool(
551 min,
552 max,
553 './tests/worker-files/thread/testWorker.js'
554 )
555 expect(
556 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
557 .workerChoiceStrategy.previousWorkerIndex
558 ).toBeUndefined()
559 expect(
560 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
561 .workerChoiceStrategy.currentWorkerIndex
562 ).toBeUndefined()
563 expect(
564 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
565 .workerChoiceStrategy.defaultWorkerWeight
566 ).toBeUndefined()
567 expect(
568 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
569 .workerChoiceStrategy.workersTaskRunTime
570 ).toBeUndefined()
571 pool.setWorkerChoiceStrategy(WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN)
572 expect(
573 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
574 .workerChoiceStrategy.previousWorkerIndex
575 ).toBe(0)
576 expect(
577 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
578 .workerChoiceStrategy.currentWorkerIndex
579 ).toBe(0)
580 expect(
581 pool.workerChoiceStrategyContext.getWorkerChoiceStrategy()
582 .workerChoiceStrategy.defaultWorkerWeight
583 ).toBeGreaterThan(0)
584 for (const worker of pool.workerChoiceStrategyContext
585 .getWorkerChoiceStrategy()
586 .workerChoiceStrategy.workersTaskRunTime.keys()) {
587 expect(
588 pool.workerChoiceStrategyContext
589 .getWorkerChoiceStrategy()
590 .workerChoiceStrategy.workersTaskRunTime.get(worker).runTime
591 ).toBe(0)
592 }
593 // We need to clean up the resources after our test
594 await pool.destroy()
595 })
596
597 it('Verify unknown strategies throw error', () => {
598 expect(
599 () =>
600 new DynamicThreadPool(
601 min,
602 max,
603 './tests/worker-files/thread/testWorker.js',
604 { workerChoiceStrategy: 'UNKNOWN_STRATEGY' }
605 )
606 ).toThrowError(
607 new Error("Worker choice strategy 'UNKNOWN_STRATEGY' not found")
608 )
609 })
610 })