├── .gitignore ├── LICENSE ├── README.md ├── doc ├── Poolee Flow.graffle └── Poolee Flow.png ├── examples └── pool_stats_wrapper.js ├── index.js ├── keep_alive_agent.js ├── lb_pool.js ├── package-lock.json ├── package.json ├── pool.js ├── pool_endpoint.js ├── pool_endpoint_request.js ├── pool_pinger.js ├── pool_request_set.js └── test ├── endpoint_test.js ├── integration_test.js ├── keep_alive_agent_test.js ├── pool_test.js ├── requestset_test.js └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Voxer IP LLC. All rights reserved. 4 | Copyright (c) 2012 Danny Coates 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lb_pool 2 | ======= 3 | 4 | In-process HTTP load balancer client with retries and backpressure for node.js. 5 | 6 | If you use HTTP for IPC, or if you consume public APIs with HTTPS, then `lb_pool` can help make your system more reliable, and possibly even faster. 7 | 8 | `lb_pool` will use HTTP/1.1 keepalive to all of the available servers, distributing the load while maintaining and reusing TCP connections as appropriate. If a request fails due to a socket error or a user-defined failure condition, the request will be retried on another node. If there are too many pending requests into a pool, new requests are failed immediately to enforce backpressure and guard against cascading failures. 9 | 10 | This module is inspired by and rewritten from [poolee](https://github.com/dannycoates/poolee), which is more actively maintained than `lb_pool`. 11 | 12 | # Usage Example 13 | 14 | ```javascript 15 | 16 | var LB_Pool = require("lb_pool"); 17 | 18 | var servers = [ 19 | "10.0.0.1:8000", 20 | "10.0.0.2:8000", 21 | "10.0.0.3:8000" 22 | ]; 23 | 24 | var auth_pool = new LB_Pool(require("http"), servers, { 25 | max_pending: 300, 26 | ping: "/ping", 27 | timeout: 10000, 28 | max_sockets: 2, 29 | name: "auth" 30 | }); 31 | 32 | auth_pool.get("/api/auth_validate?user=mjr", function (err, res, body) { 33 | // handle error or response 34 | }); 35 | ``` 36 | 37 | # Consuming external APIs with HTTPS 38 | 39 | If your application consumes public APIs, you can get better performance and reliability by using `lb_pool`: 40 | 41 | ```javascript 42 | 43 | var LB_Pool = require("lb_pool"); 44 | 45 | var servers = [ 46 | "api.facebook.com:443", 47 | "api.facebook.com:443" 48 | ]; 49 | 50 | var auth_pool = new LB_Pool(require("https"), servers, { 51 | max_pending: 10, 52 | ping: "/ping", 53 | timeout: 10000, 54 | max_sockets: 4, 55 | name: "fb" 56 | }); 57 | 58 | auth_pool.get("/me?token=aaaaaaaaaaaa", function (err, res, body) { 59 | // handle error or response 60 | }); 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /doc/Poolee Flow.graffle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ActiveLayerIndex 6 | 0 7 | ApplicationVersion 8 | 9 | com.omnigroup.OmniGrafflePro 10 | 139.16.0.171715 11 | 12 | AutoAdjust 13 | 14 | BackgroundGraphic 15 | 16 | Bounds 17 | {{0, 0}, {756, 553}} 18 | Class 19 | SolidGraphic 20 | ID 21 | 2 22 | Style 23 | 24 | shadow 25 | 26 | Draws 27 | NO 28 | 29 | stroke 30 | 31 | Draws 32 | NO 33 | 34 | 35 | 36 | BaseZoom 37 | 0 38 | CanvasOrigin 39 | {0, 0} 40 | ColumnAlign 41 | 1 42 | ColumnSpacing 43 | 36 44 | CreationDate 45 | 2013-04-22 23:46:45 +0000 46 | Creator 47 | Matthew Ranney 48 | DisplayScale 49 | 1 0/72 in = 1 0/72 in 50 | GraphDocumentVersion 51 | 8 52 | GraphicsList 53 | 54 | 55 | Bounds 56 | {{303.59693854207256, 226.68438764302414}, {91, 24}} 57 | Class 58 | ShapedGraphic 59 | FitText 60 | YES 61 | Flow 62 | Resize 63 | FontInfo 64 | 65 | Color 66 | 67 | w 68 | 0 69 | 70 | Font 71 | Helvetica 72 | Size 73 | 12 74 | 75 | ID 76 | 66 77 | Line 78 | 79 | ID 80 | 65 81 | Position 82 | 0.4633294939994812 83 | RotationType 84 | 0 85 | 86 | Shape 87 | Rectangle 88 | Style 89 | 90 | shadow 91 | 92 | Draws 93 | NO 94 | 95 | stroke 96 | 97 | Draws 98 | NO 99 | 100 | 101 | Text 102 | 103 | Text 104 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 105 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 106 | {\colortbl;\red255\green255\blue255;} 107 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 108 | 109 | \f0\fs24 \cf0 .get_endpoint()} 110 | 111 | Wrap 112 | NO 113 | 114 | 115 | Class 116 | LineGraphic 117 | Head 118 | 119 | ID 120 | 56 121 | 122 | ID 123 | 65 124 | Points 125 | 126 | {291.47625369452703, 178.02271356676738} 127 | {415.83847451962743, 308.94827488301866} 128 | 129 | Style 130 | 131 | stroke 132 | 133 | HeadArrow 134 | FilledArrow 135 | Legacy 136 | 137 | TailArrow 138 | 0 139 | 140 | 141 | Tail 142 | 143 | ID 144 | 16 145 | 146 | 147 | 148 | Class 149 | LineGraphic 150 | Head 151 | 152 | ID 153 | 15 154 | 155 | ID 156 | 64 157 | Points 158 | 159 | {471.41946789735556, 389.44976062953936} 160 | {477.20601769337372, 396.56275423649629} 161 | 162 | Style 163 | 164 | stroke 165 | 166 | HeadArrow 167 | 0 168 | Legacy 169 | 170 | TailArrow 171 | 0 172 | 173 | 174 | Tail 175 | 176 | ID 177 | 53 178 | 179 | 180 | 181 | Bounds 182 | {{432.31414806884368, 396.94810101452794}, {113.02618408203125, 17}} 183 | Class 184 | ShapedGraphic 185 | ID 186 | 15 187 | Magnets 188 | 189 | {-0.68599434921136804, -1.1433238983154379} 190 | {-0.26148819877101326, -1.3074408769607637} 191 | {0.26148823773579244, -1.3074408769607637} 192 | {0.68599429810081092, -1.1433238983154379} 193 | {1.1433238983154317, -0.6859942299534022} 194 | {1.3074408769607566, -0.26148815980625034} 195 | {1.3074408769607566, 0.26148808187668277} 196 | {1.1433237791061421, 0.6859944310174545} 197 | {0.68599436624822574, 1.1433238983154215} 198 | {0.26148792601759629, 1.3074408769607451} 199 | {-0.26148868147117577, 1.3074407577514555} 200 | {-0.68599436624822174, 1.1433238983154215} 201 | {-1.1433241367340068, 0.68599396411993652} 202 | {-1.3074408769607522, 0.26148808187668277} 203 | {-1.3074407577514626, -0.26148852561210351} 204 | {-1.1433238983154277, -0.6859942299534022} 205 | 206 | Shape 207 | Rectangle 208 | Text 209 | 210 | Text 211 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 212 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 213 | {\colortbl;\red255\green255\blue255;} 214 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 215 | 216 | \f0\b\fs24 \cf0 EndpointRequest} 217 | 218 | 219 | 220 | Bounds 221 | {{257.65187685053911, 205.85000222623944}, {33, 24}} 222 | Class 223 | ShapedGraphic 224 | FitText 225 | YES 226 | Flow 227 | Resize 228 | FontInfo 229 | 230 | Color 231 | 232 | w 233 | 0 234 | 235 | Font 236 | Helvetica 237 | Size 238 | 12 239 | 240 | ID 241 | 62 242 | Line 243 | 244 | ID 245 | 61 246 | Position 247 | 0.20427611470222473 248 | RotationType 249 | 0 250 | 251 | Shape 252 | Rectangle 253 | Style 254 | 255 | shadow 256 | 257 | Draws 258 | NO 259 | 260 | stroke 261 | 262 | Draws 263 | NO 264 | 265 | 266 | Text 267 | 268 | Text 269 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 270 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 271 | {\colortbl;\red255\green255\blue255;} 272 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 273 | 274 | \f0\fs24 \cf0 new} 275 | 276 | Wrap 277 | NO 278 | 279 | 280 | Class 281 | LineGraphic 282 | ControlPoints 283 | 284 | {2.9518230544268818, 18.743462246953015} 285 | {-14.9528748530646, -14.820021637620528} 286 | {14.952896891392072, 14.819960518078005} 287 | {-20.670152568349181, 0.85191637011922694} 288 | 289 | Head 290 | 291 | ID 292 | 15 293 | Info 294 | 14 295 | 296 | ID 297 | 61 298 | Points 299 | 300 | {255.58102443526059, 178.02271356676738} 301 | {377.78011447689789, 391.59490627697642} 302 | {431.83332102958497, 407.01096776127883} 303 | 304 | Style 305 | 306 | stroke 307 | 308 | Bezier 309 | 310 | HeadArrow 311 | FilledArrow 312 | Legacy 313 | 314 | LineType 315 | 1 316 | TailArrow 317 | 0 318 | 319 | 320 | Tail 321 | 322 | ID 323 | 16 324 | Info 325 | 14 326 | 327 | 328 | 329 | Class 330 | LineGraphic 331 | Head 332 | 333 | ID 334 | 26 335 | 336 | ID 337 | 58 338 | Points 339 | 340 | {513.73561980053648, 360.3982263303094} 341 | {609.92932779103614, 360.3982263303094} 342 | 343 | Style 344 | 345 | stroke 346 | 347 | HeadArrow 348 | FilledArrow 349 | Legacy 350 | 351 | TailArrow 352 | 0 353 | 354 | 355 | Tail 356 | 357 | ID 358 | 56 359 | 360 | 361 | 362 | Class 363 | LineGraphic 364 | Head 365 | 366 | ID 367 | 27 368 | 369 | ID 370 | 57 371 | Points 372 | 373 | {513.73561981237674, 235.62693593959986} 374 | {605.97122308858923, 235.62693593959986} 375 | 376 | Style 377 | 378 | stroke 379 | 380 | HeadArrow 381 | FilledArrow 382 | Legacy 383 | 384 | TailArrow 385 | 0 386 | 387 | 388 | Tail 389 | 390 | ID 391 | 50 392 | 393 | 394 | 395 | Bounds 396 | {{435.68278436771732, 304.30543259778784}, {52, 14}} 397 | Class 398 | ShapedGraphic 399 | FitText 400 | YES 401 | Flow 402 | Resize 403 | ID 404 | 52 405 | Shape 406 | Rectangle 407 | Style 408 | 409 | fill 410 | 411 | Draws 412 | NO 413 | 414 | shadow 415 | 416 | Draws 417 | NO 418 | 419 | stroke 420 | 421 | Draws 422 | NO 423 | 424 | 425 | Text 426 | 427 | Pad 428 | 0 429 | Text 430 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 431 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 432 | {\colortbl;\red255\green255\blue255;} 433 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 434 | 435 | \f0\b\fs24 \cf0 Endpoint} 436 | VerticalPad 437 | 0 438 | 439 | Wrap 440 | NO 441 | 442 | 443 | Bounds 444 | {{425.30508249837908, 372.45014544567886}, {78.083740234375, 16.611751556396484}} 445 | Class 446 | ShapedGraphic 447 | ID 448 | 53 449 | Shape 450 | Rectangle 451 | Text 452 | 453 | Text 454 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 455 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 456 | {\colortbl;\red255\green255\blue255;} 457 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 458 | 459 | \f0\fs24 \cf0 requests \{\}} 460 | 461 | 462 | 463 | Bounds 464 | {{425.30503952148206, 350.09437303057263}, {78.083740234375, 16.611751556396484}} 465 | Class 466 | ShapedGraphic 467 | ID 468 | 54 469 | Shape 470 | Rectangle 471 | Text 472 | 473 | Text 474 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 475 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 476 | {\colortbl;\red255\green255\blue255;} 477 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 478 | 479 | \f0\fs24 \cf0 Agent} 480 | 481 | 482 | 483 | Bounds 484 | {{425.30506120450286, 327.19989940404429}, {78.083740234375, 16.611751556396484}} 485 | Class 486 | ShapedGraphic 487 | ID 488 | 55 489 | Shape 490 | Rectangle 491 | Text 492 | 493 | Text 494 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 495 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 496 | {\colortbl;\red255\green255\blue255;} 497 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 498 | 499 | \f0\b\fs24 \cf0 Pinger} 500 | 501 | 502 | 503 | Bounds 504 | {{416.18282440252574, 299.29578132111192}, {97.05279541015625, 122.20488739013672}} 505 | Class 506 | ShapedGraphic 507 | ID 508 | 56 509 | Shape 510 | Rectangle 511 | 512 | 513 | Bounds 514 | {{435.6827825925933, 194.02219902942625}, {52, 14}} 515 | Class 516 | ShapedGraphic 517 | FitText 518 | YES 519 | Flow 520 | Resize 521 | ID 522 | 46 523 | Shape 524 | Rectangle 525 | Style 526 | 527 | fill 528 | 529 | Draws 530 | NO 531 | 532 | shadow 533 | 534 | Draws 535 | NO 536 | 537 | stroke 538 | 539 | Draws 540 | NO 541 | 542 | 543 | Text 544 | 545 | Pad 546 | 0 547 | Text 548 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 549 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 550 | {\colortbl;\red255\green255\blue255;} 551 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 552 | 553 | \f0\b\fs24 \cf0 Endpoint} 554 | VerticalPad 555 | 0 556 | 557 | Wrap 558 | NO 559 | 560 | 561 | Bounds 562 | {{425.30508072325506, 261.72372278524398}, {78.083740234375, 16.611751556396484}} 563 | Class 564 | ShapedGraphic 565 | ID 566 | 47 567 | Shape 568 | Rectangle 569 | Text 570 | 571 | Text 572 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 573 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 574 | {\colortbl;\red255\green255\blue255;} 575 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 576 | 577 | \f0\fs24 \cf0 requests \{\}} 578 | 579 | 580 | 581 | Bounds 582 | {{425.30503774635804, 239.36795037013763}, {78.083740234375, 16.611751556396484}} 583 | Class 584 | ShapedGraphic 585 | ID 586 | 48 587 | Shape 588 | Rectangle 589 | Text 590 | 591 | Text 592 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 593 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 594 | {\colortbl;\red255\green255\blue255;} 595 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 596 | 597 | \f0\fs24 \cf0 Agent} 598 | 599 | 600 | 601 | Bounds 602 | {{425.30505942937884, 216.47347674360933}, {78.083740234375, 16.611751556396484}} 603 | Class 604 | ShapedGraphic 605 | ID 606 | 49 607 | Shape 608 | Rectangle 609 | Text 610 | 611 | Text 612 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 613 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 614 | {\colortbl;\red255\green255\blue255;} 615 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 616 | 617 | \f0\b\fs24 \cf0 Pinger} 618 | 619 | 620 | 621 | Bounds 622 | {{416.18282262740172, 188.56935866067695}, {97.052797185280269, 94.115188598632812}} 623 | Class 624 | ShapedGraphic 625 | ID 626 | 50 627 | Shape 628 | Rectangle 629 | 630 | 631 | Bounds 632 | {{350.04878831135647, 51.130887773743474}, {31, 17}} 633 | Class 634 | ShapedGraphic 635 | FitText 636 | YES 637 | Flow 638 | Resize 639 | FontInfo 640 | 641 | Font 642 | Helvetica-Bold 643 | Size 644 | 12 645 | 646 | ID 647 | 44 648 | Shape 649 | Rectangle 650 | Style 651 | 652 | fill 653 | 654 | Draws 655 | NO 656 | 657 | shadow 658 | 659 | Draws 660 | NO 661 | 662 | stroke 663 | 664 | Draws 665 | NO 666 | 667 | 668 | Text 669 | 670 | Pad 671 | 0 672 | Text 673 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 674 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 675 | {\colortbl;\red255\green255\blue255;} 676 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 677 | 678 | \f0\b\fs28 \cf0 Pool} 679 | VerticalPad 680 | 0 681 | 682 | Wrap 683 | NO 684 | 685 | 686 | Class 687 | LineGraphic 688 | Head 689 | 690 | ID 691 | 17 692 | Info 693 | 15 694 | 695 | ID 696 | 37 697 | Points 698 | 699 | {136.00440083121032, 41.826300583519888} 700 | {189.98953105947146, 70.521460405963381} 701 | 702 | Style 703 | 704 | stroke 705 | 706 | HeadArrow 707 | FilledArrow 708 | Legacy 709 | 710 | TailArrow 711 | 0 712 | 713 | 714 | Tail 715 | 716 | ID 717 | 36 718 | 719 | 720 | 721 | Bounds 722 | {{67.4869084450104, 17.591623739337386}, {91, 24}} 723 | Class 724 | ShapedGraphic 725 | ID 726 | 36 727 | Shape 728 | Rectangle 729 | Text 730 | 731 | Text 732 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 733 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 734 | {\colortbl;\red255\green255\blue255;} 735 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 736 | 737 | \f0\fs24 \cf0 hs_pool.get()} 738 | 739 | 740 | 741 | Bounds 742 | {{435.68282198579163, 84.399531366244915}, {52, 14}} 743 | Class 744 | ShapedGraphic 745 | FitText 746 | YES 747 | Flow 748 | Resize 749 | ID 750 | 39 751 | Shape 752 | Rectangle 753 | Style 754 | 755 | fill 756 | 757 | Draws 758 | NO 759 | 760 | shadow 761 | 762 | Draws 763 | NO 764 | 765 | stroke 766 | 767 | Draws 768 | NO 769 | 770 | 771 | Text 772 | 773 | Pad 774 | 0 775 | Text 776 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 777 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 778 | {\colortbl;\red255\green255\blue255;} 779 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 780 | 781 | \f0\b\fs24 \cf0 Endpoint} 782 | VerticalPad 783 | 0 784 | 785 | Wrap 786 | NO 787 | 788 | 789 | Bounds 790 | {{425.30508249837908, 150.99729917113501}, {78.083740234375, 16.611751556396484}} 791 | Class 792 | ShapedGraphic 793 | ID 794 | 40 795 | Shape 796 | Rectangle 797 | Text 798 | 799 | Text 800 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 801 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 802 | {\colortbl;\red255\green255\blue255;} 803 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 804 | 805 | \f0\fs24 \cf0 requests \{\}} 806 | 807 | 808 | 809 | Bounds 810 | {{425.30503952148206, 128.64152675602861}, {78.083740234375, 16.611751556396484}} 811 | Class 812 | ShapedGraphic 813 | ID 814 | 41 815 | Shape 816 | Rectangle 817 | Text 818 | 819 | Text 820 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 821 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 822 | {\colortbl;\red255\green255\blue255;} 823 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 824 | 825 | \f0\fs24 \cf0 Agent} 826 | 827 | 828 | 829 | Bounds 830 | {{425.30506120450286, 105.74705312950026}, {78.083740234375, 16.611751556396484}} 831 | Class 832 | ShapedGraphic 833 | ID 834 | 42 835 | Shape 836 | Rectangle 837 | Text 838 | 839 | Text 840 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 841 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 842 | {\colortbl;\red255\green255\blue255;} 843 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 844 | 845 | \f0\b\fs24 \cf0 Pinger} 846 | 847 | 848 | 849 | Bounds 850 | {{416.18282440252574, 77.842935046567888}, {97.05279541015625, 94.115188598632812}} 851 | Class 852 | ShapedGraphic 853 | ID 854 | 43 855 | Shape 856 | Rectangle 857 | 858 | 859 | Class 860 | LineGraphic 861 | ControlPoints 862 | 863 | {36.802525396194767, -0.64437339702010377} 864 | {-17.560730895865618, 12.940054397619406} 865 | 866 | Head 867 | 868 | ID 869 | 26 870 | 871 | ID 872 | 32 873 | Points 874 | 875 | {545.80192984902749, 403.55593571189132} 876 | {609.96772881901882, 376.84339635323528} 877 | 878 | Style 879 | 880 | stroke 881 | 882 | Bezier 883 | 884 | HeadArrow 885 | FilledArrow 886 | Legacy 887 | 888 | LineType 889 | 1 890 | TailArrow 891 | 0 892 | 893 | 894 | Tail 895 | 896 | ID 897 | 15 898 | Info 899 | 6 900 | 901 | 902 | 903 | Class 904 | LineGraphic 905 | Head 906 | 907 | ID 908 | 28 909 | 910 | ID 911 | 29 912 | Points 913 | 914 | {513.73561983162222, 124.90051296637925} 915 | {609.92931854386063, 124.90051296637925} 916 | 917 | Style 918 | 919 | stroke 920 | 921 | HeadArrow 922 | FilledArrow 923 | Legacy 924 | 925 | TailArrow 926 | 0 927 | 928 | 929 | Tail 930 | 931 | ID 932 | 43 933 | 934 | 935 | 936 | Bounds 937 | {{610.42931856280097, 106.90052636534054}, {78.083740234375, 36}} 938 | Class 939 | ShapedGraphic 940 | ID 941 | 28 942 | Shape 943 | Rectangle 944 | Text 945 | 946 | Text 947 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 948 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 949 | {\colortbl;\red255\green255\blue255;} 950 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 951 | 952 | \f0\fs24 \cf0 HTTP Server} 953 | 954 | 955 | 956 | Bounds 957 | {{606.47122308828398, 217.62694535534808}, {78.083740234375, 36}} 958 | Class 959 | ShapedGraphic 960 | ID 961 | 27 962 | Shape 963 | Rectangle 964 | Text 965 | 966 | Text 967 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 968 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 969 | {\colortbl;\red255\green255\blue255;} 970 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 971 | 972 | \f0\fs24 \cf0 HTTP Server} 973 | 974 | 975 | 976 | Bounds 977 | {{610.42932777889064, 342.39821088269832}, {78.083740234375, 36}} 978 | Class 979 | ShapedGraphic 980 | ID 981 | 26 982 | Shape 983 | Rectangle 984 | Text 985 | 986 | Text 987 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 988 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 989 | {\colortbl;\red255\green255\blue255;} 990 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 991 | 992 | \f0\fs24 \cf0 HTTP Server} 993 | 994 | 995 | 996 | Bounds 997 | {{242.91768349041894, 102.17965587090885}, {33, 24}} 998 | Class 999 | ShapedGraphic 1000 | FitText 1001 | YES 1002 | Flow 1003 | Resize 1004 | FontInfo 1005 | 1006 | Color 1007 | 1008 | w 1009 | 0 1010 | 1011 | Font 1012 | Helvetica 1013 | Size 1014 | 12 1015 | 1016 | ID 1017 | 22 1018 | Line 1019 | 1020 | ID 1021 | 19 1022 | Position 1023 | 0.51239144802093506 1024 | RotationType 1025 | 0 1026 | 1027 | Shape 1028 | Rectangle 1029 | Style 1030 | 1031 | shadow 1032 | 1033 | Draws 1034 | NO 1035 | 1036 | stroke 1037 | 1038 | Draws 1039 | NO 1040 | 1041 | 1042 | Text 1043 | 1044 | Text 1045 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 1046 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1047 | {\colortbl;\red255\green255\blue255;} 1048 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1049 | 1050 | \f0\fs24 \cf0 new} 1051 | 1052 | Wrap 1053 | NO 1054 | 1055 | 1056 | Class 1057 | LineGraphic 1058 | Head 1059 | 1060 | ID 1061 | 16 1062 | 1063 | ID 1064 | 19 1065 | Points 1066 | 1067 | {244.58952238103518, 84.92146383919092} 1068 | {273.52864922762126, 142.02271356676738} 1069 | 1070 | Style 1071 | 1072 | stroke 1073 | 1074 | HeadArrow 1075 | FilledArrow 1076 | Legacy 1077 | 1078 | TailArrow 1079 | 0 1080 | 1081 | 1082 | Tail 1083 | 1084 | ID 1085 | 17 1086 | 1087 | 1088 | 1089 | Bounds 1090 | {{189.98953105947146, 60.921463839190913}, {91, 24}} 1091 | Class 1092 | ShapedGraphic 1093 | ID 1094 | 17 1095 | Magnets 1096 | 1097 | {-0.68599434921136948, -1.1433238983154297} 1098 | {-0.26148819877101503, -1.3074408769607544} 1099 | {0.26148823773579066, -1.3074408769607544} 1100 | {0.68599429810080947, -1.1433238983154297} 1101 | {1.1433238983154301, -0.6859942299533941} 1102 | {1.3074408769607551, -0.26148815980624107} 1103 | {1.3074408769607551, 0.26148808187669204} 1104 | {1.1433237791061406, 0.6859944310174626} 1105 | {0.6859943662482243, 1.1433238983154297} 1106 | {0.26148792601759452, 1.3074408769607544} 1107 | {-0.26148868147117754, 1.3074407577514648} 1108 | {-0.68599436624822319, 1.1433238983154297} 1109 | {-1.1433241367340083, 0.68599396411994462} 1110 | {-1.3074408769607537, 0.26148808187669204} 1111 | {-1.3074407577514642, -0.26148852561209424} 1112 | {-1.1433238983154292, -0.6859942299533941} 1113 | 1114 | Shape 1115 | Rectangle 1116 | Text 1117 | 1118 | Text 1119 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 1120 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1121 | {\colortbl;\red255\green255\blue255;} 1122 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1123 | 1124 | \f0\fs24 \cf0 .request()} 1125 | 1126 | 1127 | 1128 | Bounds 1129 | {{219.68582405953521, 142.02271356676738}, {107.68564605712891, 36}} 1130 | Class 1131 | ShapedGraphic 1132 | ID 1133 | 16 1134 | Magnets 1135 | 1136 | {-0.7396002417658698, -1.1094003915786734} 1137 | {-0.42163697454145854, -1.2649110555648793} 1138 | {1.9868215517249155e-08, -1.3333333730697621} 1139 | {0.42163710648196329, -1.2649110555648793} 1140 | {0.73960031615696309, -1.1094003915786734} 1141 | {1.1094003915786743, -0.73960025003154684} 1142 | {1.2649110555648804, -0.4216368426009538} 1143 | {1.3333333730697632, 1.5894572413799324e-07} 1144 | {1.2649110555648804, 0.42163714417925036} 1145 | {1.1094003915786743, 0.73960034921967122} 1146 | {0.73960021696883871, 1.1094003915786743} 1147 | {0.42163699339010208, 1.2649110555648804} 1148 | {0, 1.3333333730697632} 1149 | {-0.42163733062650977, 1.2649109363555908} 1150 | {-0.73960021696883871, 1.1094003915786743} 1151 | {-1.1094003915786743, 0.73960034921967122} 1152 | {-1.2649110555648804, 0.42163676720637966} 1153 | {-1.3333333730697632, -6.3578289655197295e-07} 1154 | {-1.2649109363555908, -0.4216371798373757} 1155 | {-1.1094005107879639, -0.73960016419084695} 1156 | 1157 | Shape 1158 | Rectangle 1159 | Text 1160 | 1161 | Text 1162 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 1163 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1164 | {\colortbl;\red255\green255\blue255;} 1165 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1166 | 1167 | \f0\b\fs24 \cf0 RequestSet} 1168 | 1169 | 1170 | 1171 | Bounds 1172 | {{204.94241577425424, 40.774866500799718}, {321.21273803710938, 402.40838623046875}} 1173 | Class 1174 | ShapedGraphic 1175 | ID 1176 | 1 1177 | Shape 1178 | Rectangle 1179 | 1180 | 1181 | GridInfo 1182 | 1183 | GuidesLocked 1184 | NO 1185 | GuidesVisible 1186 | YES 1187 | HPages 1188 | 1 1189 | ImageCounter 1190 | 1 1191 | KeepToScale 1192 | 1193 | Layers 1194 | 1195 | 1196 | Lock 1197 | NO 1198 | Name 1199 | Layer 1 1200 | Print 1201 | YES 1202 | View 1203 | YES 1204 | 1205 | 1206 | LayoutInfo 1207 | 1208 | Animate 1209 | NO 1210 | circoMinDist 1211 | 18 1212 | circoSeparation 1213 | 0.0 1214 | layoutEngine 1215 | dot 1216 | neatoSeparation 1217 | 0.0 1218 | twopiSeparation 1219 | 0.0 1220 | 1221 | LinksVisible 1222 | NO 1223 | MagnetsVisible 1224 | NO 1225 | MasterSheets 1226 | 1227 | ModificationDate 1228 | 2013-04-23 00:17:57 +0000 1229 | Modifier 1230 | Matthew Ranney 1231 | NotesVisible 1232 | NO 1233 | Orientation 1234 | 2 1235 | OriginVisible 1236 | NO 1237 | PageBreaks 1238 | YES 1239 | PrintInfo 1240 | 1241 | NSBottomMargin 1242 | 1243 | float 1244 | 41 1245 | 1246 | NSHorizonalPagination 1247 | 1248 | coded 1249 | BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG 1250 | 1251 | NSLeftMargin 1252 | 1253 | float 1254 | 18 1255 | 1256 | NSOrientation 1257 | 1258 | coded 1259 | BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwGG 1260 | 1261 | NSPaperSize 1262 | 1263 | size 1264 | {792, 612} 1265 | 1266 | NSPrintReverseOrientation 1267 | 1268 | int 1269 | 0 1270 | 1271 | NSRightMargin 1272 | 1273 | float 1274 | 18 1275 | 1276 | NSTopMargin 1277 | 1278 | float 1279 | 18 1280 | 1281 | 1282 | PrintOnePage 1283 | 1284 | ReadOnly 1285 | NO 1286 | RowAlign 1287 | 1 1288 | RowSpacing 1289 | 36 1290 | SheetTitle 1291 | Canvas 1 1292 | SmartAlignmentGuidesActive 1293 | YES 1294 | SmartDistanceGuidesActive 1295 | YES 1296 | UniqueID 1297 | 1 1298 | UseEntirePage 1299 | 1300 | VPages 1301 | 1 1302 | WindowInfo 1303 | 1304 | CurrentSheet 1305 | 0 1306 | ExpandedCanvases 1307 | 1308 | 1309 | name 1310 | Canvas 1 1311 | 1312 | 1313 | FitInWindow 1314 | 1315 | Frame 1316 | {{57, 13}, {1837, 1405}} 1317 | ListView 1318 | 1319 | OutlineWidth 1320 | 142 1321 | RightSidebar 1322 | 1323 | Sidebar 1324 | 1325 | SidebarWidth 1326 | 120 1327 | VisibleRegion 1328 | {{0.4397905934834343, -11.560209406516567}, {755.12044901105673, 576.56546805678238}} 1329 | Zoom 1330 | 2.2738094329833984 1331 | ZoomValues 1332 | 1333 | 1334 | Canvas 1 1335 | 0.0 1336 | 1 1337 | 1338 | 1339 | 1340 | 1341 | 1342 | -------------------------------------------------------------------------------- /doc/Poolee Flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber-node/lb_pool/b2f4b431f212621544bfb39f3116a37f1d27c856/doc/Poolee Flow.png -------------------------------------------------------------------------------- /examples/pool_stats_wrapper.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Voxer IP LLC. All rights reserved. 2 | 3 | // Voxer specific logging and metrics around the generic poolee library 4 | 5 | var RV; 6 | var inherits = require("util").inherits; 7 | var log, warn, error, histogram, counter; 8 | var Pool = require("./lb_pool")(RV); 9 | 10 | function PooleeStatsWrapper(http, nodes, options) { 11 | var self = this; 12 | 13 | options = options || {}; 14 | if (!options.hasOwnProperty("keep_alive")) { 15 | options.keep_alive = true; 16 | } 17 | Pool.call(this, http, nodes, options); 18 | 19 | this.interval_timer = setInterval(function () { 20 | self.emit_stats(); 21 | }, options.stat_interval || 60000); 22 | 23 | this.on("retrying", function (err) { 24 | var path = err.attempt.options.path; 25 | log(this.name + " retry", "path=" + path + " reason=" + err.message); 26 | counter("LB_Pool>retry|" + this.name + "|" + err.reason.replace(/\W/g, "_")); 27 | }); 28 | 29 | this.on("health", function (status) { 30 | log(this.name + " health", "status=" + status); 31 | }); 32 | 33 | this.on("timeout", function (uri, state) { 34 | log(this.name + " timeout", "uri=" + uri + " state=" + state); 35 | counter("LB_Pool>timeout>" + this.name + "|" + state); 36 | }); 37 | 38 | this.on("timing", function (duration, request_options) { 39 | var path = request_options.path.split("?")[0]; 40 | histogram("LB_Pool>timing>" + this.name + "|" + (options.no_metric_path ? request_options.method : path), duration); 41 | if (request_options.reused) { 42 | counter("LB_Pool>sockets>reused|" + this.name); 43 | } else { 44 | counter("LB_Pool>sockets>created|" + this.name); 45 | } 46 | if (!request_options.success) { 47 | histogram("LB_Pool>failed>" + this.name, duration); 48 | log(this.name + " request failed", "host=" + request_options.host + " path=" + request_options.path + " duration=" + duration); 49 | } 50 | if (duration > 200) { // TODO - magic number alert 51 | log("timing_stats", "method=" + request_options.method + " path=" + request_options.path + " endpoint=" + request_options.host + ":" + request_options.port + " duration=" + duration); 52 | } 53 | }); 54 | } 55 | inherits(PooleeStatsWrapper, Pool); 56 | 57 | PooleeStatsWrapper.prototype.emit_stats = function () { 58 | var all_stats = this.stats(), endpoint, i; 59 | var total_pending = 0, total_sockets = 0, total_unhealthy = 0; 60 | 61 | for (i = 0; i < all_stats.length; i++) { 62 | endpoint = all_stats[i]; 63 | total_pending += endpoint.pending; 64 | total_sockets += endpoint.socket_request_counts.length; 65 | if (! endpoint.healthy) { 66 | total_unhealthy++; 67 | } 68 | } 69 | histogram("LB_Pool>stats>pending_total|" + this.name, total_pending); 70 | histogram("LB_Pool>stats>sockets_total|" + this.name, total_sockets); 71 | histogram("LB_Pool>stats>unhealthy_total|" + this.name, total_unhealthy); 72 | }; 73 | 74 | module.exports = function init(new_RV) { 75 | RV = new_RV; 76 | 77 | var metrics = require("./metrics_client"); 78 | histogram = metrics.histogram; 79 | counter = metrics.counter; 80 | 81 | var logger = require("./logger"); 82 | log = logger.log; 83 | warn = logger.warn; 84 | error = logger.error; 85 | 86 | return PooleeStatsWrapper; 87 | }; 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.Pool = require("./lb_pool")({}); 2 | -------------------------------------------------------------------------------- /keep_alive_agent.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var http = require("http"), 4 | https = require("https"), 5 | inherits = require("util").inherits; 6 | 7 | function KeepAliveAgent(options) { 8 | options = options || {}; 9 | http.Agent.call(this, options); 10 | 11 | this.keepAlive = true; 12 | 13 | this.max_reqs_per_socket = options.max_reqs_per_socket || 1000; 14 | 15 | // Keys are host:port names, values are lists of sockets. 16 | this.idle_sockets = {}; 17 | 18 | // Replace the 'free' listener set up by the default node Agent above. 19 | this.removeAllListeners("free"); 20 | 21 | var self = this; 22 | this.on("free", function (socket, host, port, local_address) { 23 | self.on_free(socket, host, port, local_address); 24 | }); 25 | } 26 | inherits(KeepAliveAgent, http.Agent); 27 | 28 | // http.Agent has a destroy() method in node 0.12 but not in node 0.10 29 | if (!KeepAliveAgent.prototype.destroy) { 30 | KeepAliveAgent.prototype.destroy = function destroy() { 31 | var self = this; 32 | if (this.sockets) { 33 | Object.keys(this.sockets).forEach(function (key) { 34 | var socks = self.sockets[key]; 35 | if (socks) { 36 | socks.forEach(function (sock) { 37 | sock.unref(); 38 | }); 39 | } 40 | }); 41 | } 42 | }; 43 | } 44 | 45 | KeepAliveAgent.prototype.build_name_key = function (host, port, local_address) { 46 | if (typeof host !== 'string') { 47 | port = host.port; 48 | local_address = host.localAddress; 49 | host = host.host; 50 | } 51 | 52 | var name = host + ":" + port; 53 | if (local_address) { 54 | name += ":" + local_address; 55 | } 56 | return name; 57 | }; 58 | 59 | // socket reuse strategy: 60 | // after a request is finished, decide whether to preserve this socket 61 | // if socket is "usable", meaning node didn't mark it as destroyed, 62 | // check for max request_count, and destroy as necessary 63 | KeepAliveAgent.prototype.on_free = function (socket, host, port, local_address) { 64 | var name = this.build_name_key(host, port, local_address); 65 | 66 | if (this.is_socket_usable(socket)) { 67 | socket.request_count = socket.request_count ? socket.request_count + 1 : 1; 68 | 69 | if (socket.request_count >= this.max_reqs_per_socket) { 70 | socket.destroy(); 71 | } else { 72 | if (!this.idle_sockets[name]) { 73 | this.idle_sockets[name] = []; 74 | } 75 | this.idle_sockets[name].push(socket); 76 | } 77 | } 78 | 79 | // If we had any pending requests for this name, send the next one off now. 80 | if (this.requests[name] && this.requests[name].length) { 81 | var next_request = this.requests[name].shift(); 82 | 83 | if (!this.requests[name].length) { 84 | delete this.requests[name]; 85 | } 86 | 87 | this.addRequest(next_request, host, port, local_address); 88 | } 89 | }; 90 | 91 | // addRequest is called by from node in http.js. We intercept this and re-use a socket if we've got one available. 92 | KeepAliveAgent.prototype.addRequest = function (request, host, port, local_address) { 93 | var name = this.build_name_key(host, port, local_address); 94 | var socket = this.next_idle_socket(name); 95 | 96 | if (socket) { 97 | request.onSocket(socket); 98 | } else { 99 | http.Agent.prototype.addRequest.call(this, request, host, port, local_address); 100 | } 101 | }; 102 | 103 | KeepAliveAgent.prototype.next_idle_socket = function (name) { 104 | if (!this.idle_sockets[name]) { 105 | return null; 106 | } 107 | 108 | var socket; 109 | while ((socket = this.idle_sockets[name].shift()) !== undefined) { 110 | // Check that this socket is still healthy after sitting around on the shelf. 111 | if (this.is_socket_usable(socket)) { 112 | return socket; 113 | } 114 | } 115 | return null; 116 | }; 117 | 118 | KeepAliveAgent.prototype.is_socket_usable = function (socket) { 119 | return !socket.destroyed; 120 | }; 121 | 122 | // removeSocket is called from node in http.js. We intercept to update the idle_sockets map. 123 | KeepAliveAgent.prototype.removeSocket = function (socket, name, host, port, local_address) { 124 | if (this.idle_sockets[name]) { 125 | var idx = this.idle_sockets[name].indexOf(socket); 126 | if (idx !== -1) { 127 | this.idle_sockets[name].splice(idx, 1); 128 | if (!this.idle_sockets[name].length) { 129 | delete this.idle_sockets[name]; 130 | } 131 | } 132 | } 133 | 134 | http.Agent.prototype.removeSocket.call(this, socket, name, host, port, local_address); 135 | }; 136 | 137 | 138 | function HTTPSKeepAliveAgent(options) { 139 | KeepAliveAgent.call(this, options); 140 | this.createConnection = https.globalAgent.createConnection; // node Agent API 141 | } 142 | inherits(HTTPSKeepAliveAgent, KeepAliveAgent); 143 | 144 | // defaultPort is part of the node API for Agent 145 | HTTPSKeepAliveAgent.prototype.defaultPort = 443; 146 | 147 | HTTPSKeepAliveAgent.prototype.is_socket_usable = function (socket) { 148 | // TLS sockets null out their secure pair's ssl field in destroy() and do not set destroyed the way non-secure sockets do. 149 | return socket.pair && socket.pair.ssl; 150 | }; 151 | 152 | module.exports = function init() { 153 | return { 154 | HTTP: KeepAliveAgent, 155 | HTTPS: HTTPSKeepAliveAgent 156 | }; 157 | }; 158 | -------------------------------------------------------------------------------- /lb_pool.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var GO; // global object for attaching to a REPL and finding in core dumps 4 | 5 | module.exports = function init(new_GO) { 6 | GO = new_GO || {}; 7 | 8 | GO.KeepAliveAgent = require("./keep_alive_agent")(GO); 9 | GO.PoolPinger = require("./pool_pinger")(GO); 10 | GO.PoolEndpoint = require("./pool_endpoint")(GO); 11 | GO.PoolEndpointRequest = require("./pool_endpoint_request")(GO); 12 | GO.PoolRequestSet = require("./pool_request_set")(GO); 13 | 14 | return require("./pool")(GO); 15 | }; 16 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lb_pool", 3 | "version": "1.8.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "mocha": { 8 | "version": "3.1.0", 9 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.1.0.tgz", 10 | "integrity": "sha1-EvJW/qiF4WoeYnoXHWi+7w8tYYw=", 11 | "dev": true, 12 | "requires": { 13 | "browser-stdout": "1.3.0", 14 | "commander": "2.9.0", 15 | "debug": "2.2.0", 16 | "diff": "1.4.0", 17 | "escape-string-regexp": "1.0.5", 18 | "glob": "7.0.5", 19 | "growl": "1.9.2", 20 | "json3": "3.3.2", 21 | "lodash.create": "3.1.1", 22 | "mkdirp": "0.5.1", 23 | "supports-color": "3.1.2" 24 | }, 25 | "dependencies": { 26 | "browser-stdout": { 27 | "version": "1.3.0", 28 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 29 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 30 | "dev": true 31 | }, 32 | "commander": { 33 | "version": "2.9.0", 34 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 35 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 36 | "dev": true, 37 | "requires": { 38 | "graceful-readlink": ">= 1.0.0" 39 | }, 40 | "dependencies": { 41 | "graceful-readlink": { 42 | "version": "1.0.1", 43 | "resolved": "http://archive.local.uber.internal/npm/graceful-readlink/graceful-readlink-1.0.1.tgz", 44 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 45 | "dev": true 46 | } 47 | } 48 | }, 49 | "debug": { 50 | "version": "2.2.0", 51 | "resolved": "http://archive.local.uber.internal/npm/debug/debug-2.2.0.tgz", 52 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 53 | "dev": true, 54 | "requires": { 55 | "ms": "0.7.1" 56 | }, 57 | "dependencies": { 58 | "ms": { 59 | "version": "0.7.1", 60 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 61 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", 62 | "dev": true 63 | } 64 | } 65 | }, 66 | "diff": { 67 | "version": "1.4.0", 68 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", 69 | "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", 70 | "dev": true 71 | }, 72 | "escape-string-regexp": { 73 | "version": "1.0.5", 74 | "resolved": "http://archive.local.uber.internal/npm/escape-string-regexp/escape-string-regexp-1.0.5.tgz", 75 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 76 | "dev": true 77 | }, 78 | "glob": { 79 | "version": "7.0.5", 80 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", 81 | "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", 82 | "dev": true, 83 | "requires": { 84 | "fs.realpath": "^1.0.0", 85 | "inflight": "^1.0.4", 86 | "inherits": "2", 87 | "minimatch": "^3.0.2", 88 | "once": "^1.3.0", 89 | "path-is-absolute": "^1.0.0" 90 | }, 91 | "dependencies": { 92 | "fs.realpath": { 93 | "version": "1.0.0", 94 | "resolved": "http://archive.local.uber.internal/npm/fs.realpath/fs.realpath-1.0.0.tgz", 95 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 96 | "dev": true 97 | }, 98 | "inflight": { 99 | "version": "1.0.5", 100 | "resolved": "http://archive.local.uber.internal/npm/inflight/inflight-1.0.5.tgz", 101 | "integrity": "sha1-2zIEzVqd4ubNiQuFxuL2a89PYgo=", 102 | "dev": true, 103 | "requires": { 104 | "once": "^1.3.0", 105 | "wrappy": "1" 106 | }, 107 | "dependencies": { 108 | "wrappy": { 109 | "version": "1.0.2", 110 | "resolved": "http://archive.local.uber.internal/npm/wrappy/wrappy-1.0.2.tgz", 111 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 112 | "dev": true 113 | } 114 | } 115 | }, 116 | "inherits": { 117 | "version": "2.0.3", 118 | "resolved": "http://archive.local.uber.internal/npm/inherits/inherits-2.0.3.tgz", 119 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 120 | "dev": true 121 | }, 122 | "minimatch": { 123 | "version": "3.0.3", 124 | "resolved": "http://archive.local.uber.internal/npm/minimatch/minimatch-3.0.3.tgz", 125 | "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", 126 | "dev": true, 127 | "requires": { 128 | "brace-expansion": "^1.0.0" 129 | }, 130 | "dependencies": { 131 | "brace-expansion": { 132 | "version": "1.1.6", 133 | "resolved": "http://archive.local.uber.internal/npm/brace-expansion/brace-expansion-1.1.6.tgz", 134 | "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", 135 | "dev": true, 136 | "requires": { 137 | "balanced-match": "^0.4.1", 138 | "concat-map": "0.0.1" 139 | }, 140 | "dependencies": { 141 | "balanced-match": { 142 | "version": "0.4.2", 143 | "resolved": "http://archive.local.uber.internal/npm/balanced-match/balanced-match-0.4.2.tgz", 144 | "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", 145 | "dev": true 146 | }, 147 | "concat-map": { 148 | "version": "0.0.1", 149 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 150 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 151 | "dev": true 152 | } 153 | } 154 | } 155 | } 156 | }, 157 | "once": { 158 | "version": "1.4.0", 159 | "resolved": "http://archive.local.uber.internal/npm/once/once-1.4.0.tgz", 160 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 161 | "dev": true, 162 | "requires": { 163 | "wrappy": "1" 164 | }, 165 | "dependencies": { 166 | "wrappy": { 167 | "version": "1.0.2", 168 | "resolved": "http://archive.local.uber.internal/npm/wrappy/wrappy-1.0.2.tgz", 169 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 170 | "dev": true 171 | } 172 | } 173 | }, 174 | "path-is-absolute": { 175 | "version": "1.0.1", 176 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 177 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 178 | "dev": true 179 | } 180 | } 181 | }, 182 | "growl": { 183 | "version": "1.9.2", 184 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 185 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 186 | "dev": true 187 | }, 188 | "json3": { 189 | "version": "3.3.2", 190 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", 191 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", 192 | "dev": true 193 | }, 194 | "lodash.create": { 195 | "version": "3.1.1", 196 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", 197 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", 198 | "dev": true, 199 | "requires": { 200 | "lodash._baseassign": "^3.0.0", 201 | "lodash._basecreate": "^3.0.0", 202 | "lodash._isiterateecall": "^3.0.0" 203 | }, 204 | "dependencies": { 205 | "lodash._baseassign": { 206 | "version": "3.2.0", 207 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", 208 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", 209 | "dev": true, 210 | "requires": { 211 | "lodash._basecopy": "^3.0.0", 212 | "lodash.keys": "^3.0.0" 213 | }, 214 | "dependencies": { 215 | "lodash._basecopy": { 216 | "version": "3.0.1", 217 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", 218 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", 219 | "dev": true 220 | }, 221 | "lodash.keys": { 222 | "version": "3.1.2", 223 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", 224 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", 225 | "dev": true, 226 | "requires": { 227 | "lodash._getnative": "^3.0.0", 228 | "lodash.isarguments": "^3.0.0", 229 | "lodash.isarray": "^3.0.0" 230 | }, 231 | "dependencies": { 232 | "lodash._getnative": { 233 | "version": "3.9.1", 234 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", 235 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", 236 | "dev": true 237 | }, 238 | "lodash.isarguments": { 239 | "version": "3.1.0", 240 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 241 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", 242 | "dev": true 243 | }, 244 | "lodash.isarray": { 245 | "version": "3.0.4", 246 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", 247 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", 248 | "dev": true 249 | } 250 | } 251 | } 252 | } 253 | }, 254 | "lodash._basecreate": { 255 | "version": "3.0.3", 256 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", 257 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", 258 | "dev": true 259 | }, 260 | "lodash._isiterateecall": { 261 | "version": "3.0.9", 262 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", 263 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", 264 | "dev": true 265 | } 266 | } 267 | }, 268 | "mkdirp": { 269 | "version": "0.5.1", 270 | "resolved": "http://archive.local.uber.internal/npm/mkdirp/mkdirp-0.5.1.tgz", 271 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 272 | "dev": true, 273 | "requires": { 274 | "minimist": "0.0.8" 275 | }, 276 | "dependencies": { 277 | "minimist": { 278 | "version": "0.0.8", 279 | "resolved": "http://archive.local.uber.internal/npm/minimist/minimist-0.0.8.tgz", 280 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 281 | "dev": true 282 | } 283 | } 284 | }, 285 | "supports-color": { 286 | "version": "3.1.2", 287 | "resolved": "http://archive.local.uber.internal/npm/supports-color/supports-color-3.1.2.tgz", 288 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", 289 | "dev": true, 290 | "requires": { 291 | "has-flag": "^1.0.0" 292 | }, 293 | "dependencies": { 294 | "has-flag": { 295 | "version": "1.0.0", 296 | "resolved": "http://archive.local.uber.internal/npm/has-flag/has-flag-1.0.0.tgz", 297 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 298 | "dev": true 299 | } 300 | } 301 | } 302 | } 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lb_pool", 3 | "version": "1.8.0", 4 | "description": "in-process HTTP load balancer client with retries", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Voxer/lb_pool.git" 8 | }, 9 | "scripts": { 10 | "test": "bash test/run.sh" 11 | }, 12 | "devDependencies": { 13 | "mocha": ">=1.8.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pool.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var GO, // GO is global object, for passing to a REPL or finding in a core dump 4 | EventEmitter = require("events").EventEmitter, 5 | Stream = require("stream"), 6 | inherits = require("util").inherits; 7 | 8 | // Pool - manages a set of equivalent endpoints and distributes requests among them 9 | // 10 | // http: which endpoint HTTP module to use, either http.js or https.js 11 | // endpoints: array of strings formatted like "ip:port" 12 | // options: { 13 | // max_pending: number of pending requests allowed (1000) 14 | // max_sockets: number of sockets per Endpoint (5) 15 | // ping: ping path (default = no ping checks) 16 | // ping_timeout: number (milliseconds) default 2000 17 | // retry_filter: function (response) { return true to reject response and retry } 18 | // retry_delay: number (milliseconds) default 20 19 | // keep_alive: use an alternate Agent that does keep-alive properly (boolean) default false 20 | // name: string (optional) 21 | // max_retries: number (default = 5) 22 | // agent_options: {} an object for passing options directly to the HTTP(S) Agent 23 | // } 24 | function Pool(http, endpoints, options) { 25 | if (!http || !http.request || !http.Agent) { 26 | throw new Error("invalid http module"); 27 | } 28 | if (! Array.isArray(endpoints)) { 29 | throw new Error("endpoints must be an array"); 30 | } 31 | this.http = http; 32 | 33 | options = options || {}; 34 | options.retry_filter = options.retry_filter || options.retryFilter; 35 | options.retry_delay = options.retry_delay || options.retryDelay; 36 | options.ping = options.ping || options.path; 37 | if (typeof options.max_retries === "number") { 38 | options.max_retries = options.max_retries; 39 | } else if (typeof options.maxRetries === "number") { 40 | options.max_retries = options.maxRetries; 41 | } else { 42 | options.max_retries = 5; 43 | } 44 | 45 | // retry_delay can be 0, which is useful but also falsy so we check for 0 explicitly 46 | if (!options.retry_delay && options.retry_delay !== 0) { 47 | options.retry_delay = 20; 48 | } 49 | 50 | this.name = options.name; 51 | this.options = options; 52 | this.max_pending = options.max_pending || options.maxPending || 1000; 53 | this.max_pool_size = options.max_pool_size || options.maxPoolSize; 54 | this.endpoints = []; 55 | this.endpoints_by_name = {}; 56 | 57 | // clone 58 | this.all_hostports = endpoints.slice(); 59 | 60 | if (this.max_pool_size !== undefined && this.max_pool_size !== null && 61 | (typeof this.max_pool_size !== 'number' || this.max_pool_size < 1)) { 62 | throw new Error('max_pool_size invalid'); 63 | } 64 | 65 | var selectedEndpoints = endpoints; 66 | 67 | if (this.max_pool_size < endpoints.length) { 68 | // simple resevoir sampling algorithm 69 | selectedEndpoints = new Array(this.max_pool_size); 70 | for (var j = 0; j < endpoints.length; j++) { 71 | if (j < this.max_pool_size) { 72 | selectedEndpoints[j] = endpoints[j]; 73 | } else { 74 | // random replacement with decreasing probablity 75 | var r = Math.floor(Math.random() * (j + 1)); 76 | if (r < this.max_pool_size) { 77 | selectedEndpoints[r] = endpoints[j]; 78 | } 79 | } 80 | } 81 | } 82 | 83 | this.length = 0; 84 | for (var i = 0; i < selectedEndpoints.length; i++) { 85 | this.add_pool_endpoint(selectedEndpoints[i]); 86 | } 87 | 88 | if (this.endpoints.length === 0) { 89 | throw new Error("no valid endpoints"); 90 | } 91 | 92 | // this special endpoint is returned when the pool is overloaded 93 | this.overloaded_endpoint = new GO.PoolEndpoint({Agent: Object}, null, null, {timeout: 0}); 94 | this.overloaded_endpoint.special_endpoint = "overloaded"; 95 | this.overloaded_endpoint.healthy = false; 96 | this.overloaded_endpoint.request = function (options, callback) { 97 | var err = new Error("too many pending requests"); 98 | err.reason = "full"; 99 | err.delay = true; 100 | err.attempt = { options: options }; 101 | process.nextTick(function () { 102 | callback(err); 103 | }); 104 | }; 105 | 106 | // this special endpoint is returned when there are no healthy endpoints 107 | this.unhealthy_endpoint = new GO.PoolEndpoint({Agent: Object}, null, null, {timeout: 0}); 108 | this.unhealthy_endpoint.special_endpoint = "unhealthy"; 109 | this.unhealthy_endpoint.healthy = false; 110 | this.unhealthy_endpoint.request = function (options, callback) { 111 | var err = new Error("no healthy endpoints"); 112 | err.reason = "unhealthy"; 113 | err.delay = true; 114 | err.attempt = { options: options }; 115 | process.nextTick(function () { 116 | callback(err); 117 | }); 118 | }; 119 | } 120 | inherits(Pool, EventEmitter); 121 | 122 | Pool.prototype.endpoint_health_changed = function endpoint_health_changed(endpoint) { 123 | this.emit("health", endpoint.name + " health: " + endpoint.healthy); 124 | }; 125 | 126 | Pool.prototype.endpoint_timed_out = function endpoint_timed_out(request) { 127 | this.emit("timeout", request); 128 | }; 129 | 130 | // returns an array of healthy Endpoints 131 | Pool.prototype.healthy_endpoints = function health_endpoints() { 132 | var healthy = [], 133 | len = this.endpoints.length; 134 | 135 | for (var i = 0; i < len; i++) { 136 | var n = this.endpoints[i]; 137 | if (n.healthy) { 138 | healthy.push(n); 139 | } 140 | } 141 | return healthy; 142 | }; 143 | 144 | Pool.prototype.on_retry = function on_retry(err) { 145 | this.emit("retrying", err); 146 | }; 147 | 148 | // options: { 149 | // path: string 150 | // method: ["POST", "GET", "PUT", "DELETE", "HEAD"] (GET) 151 | // retryFilter: function (response) { return true to reject response and retry } 152 | // attempts: number (optional, default = endpoints.length) 153 | // retryDelay: number (milliseconds) default Pool.retry_delay 154 | // timeout: request timeout in ms 155 | // encoding: response body encoding (utf8) 156 | // } 157 | // data: string or buffer 158 | // 159 | // callback: 160 | // function(err, res, body) {} 161 | // function(err, res) {} 162 | 163 | // for convenience, we allow an option string to be the "path" option 164 | Pool.prototype.init_req_options = function init_req_options(options) { 165 | if (! options) { 166 | return {}; 167 | } 168 | if (typeof options === "string") { 169 | return { path: options }; 170 | } 171 | return options; 172 | }; 173 | 174 | Pool.prototype.request = function request(options, data, callback) { 175 | var self = this; 176 | 177 | options = this.init_req_options(options); 178 | 179 | // data is optional 180 | if (!options.data && (typeof data === "string" || Buffer.isBuffer(data) || data instanceof Stream)) { 181 | options.data = data; 182 | } else if (typeof data === "function") { 183 | callback = data; 184 | } 185 | 186 | if (typeof callback !== "function") { 187 | throw new Error("a callback is required"); 188 | } 189 | 190 | options.method = options.method || "GET"; 191 | 192 | options.retry_delay = options.retry_delay || options.retryDelay; 193 | if (!options.retry_delay && options.retry_delay !== 0) { 194 | options.retry_delay = this.options.retry_delay; 195 | } 196 | 197 | options.retry_filter = options.retry_filter || options.retryFilter; 198 | if (!options.retry_filter) { 199 | options.retry_filter = this.options.retry_filter; 200 | } 201 | 202 | var req_set = new GO.PoolRequestSet(this, options, function (err, res, body) { 203 | options.success = !err; 204 | if (res && res.socket && res.socket.request_count && res.socket.request_count > 1) { 205 | options.reused = true; 206 | } else { 207 | options.reused = false; 208 | } 209 | self.emit("timing", req_set.duration, options); 210 | self.emit("response", err, req_set, res); 211 | callback(err, res, body); 212 | }); 213 | return req_set.do_request(); 214 | }; 215 | 216 | Pool.prototype.get = Pool.prototype.request; 217 | 218 | Pool.prototype.put = function put(options, data, callback) { 219 | options = this.init_req_options(options); // note that this will call init_req_options twice, which is fine 220 | options.method = "PUT"; 221 | return this.request(options, data, callback); 222 | }; 223 | 224 | Pool.prototype.post = function post(options, data, callback) { 225 | options = this.init_req_options(options); 226 | options.method = "POST"; 227 | return this.request(options, data, callback); 228 | }; 229 | 230 | Pool.prototype.del = function del(options, callback) { 231 | options = this.init_req_options(options); 232 | options.method = "DELETE"; 233 | return this.request(options, callback); 234 | }; 235 | 236 | Pool.prototype.stats = function stats() { 237 | var stats = []; 238 | var len = this.endpoints.length; 239 | for (var i = 0; i < len; i++) { 240 | var endpoint = this.endpoints[i]; 241 | stats.push(endpoint.stats()); 242 | } 243 | return stats; 244 | }; 245 | 246 | // endpoint selection strategy: 247 | // start at a random point in the list of all endpoints 248 | // walk through and use the first endpoint we find that is ready() 249 | // if none are ready, then check max_pending limits 250 | // walk the list of healthy endpoints, returning the first endpoint with below average pending 251 | Pool.prototype.get_endpoint = function get_endpoint(options) { 252 | var endpoints_len = this.endpoints.length; 253 | var min_pending_level = Infinity, min_pending_endpoint = null; 254 | var total_pending = 0; 255 | var endpoint_pos = Math.floor(Math.random() * endpoints_len); 256 | var i, endpoint; 257 | 258 | options = options || {}; 259 | if (options.endpoint) { 260 | endpoint = this.endpoints_by_name[options.endpoint]; 261 | if (!endpoint) { 262 | return this.unhealthy_endpoint; 263 | } 264 | if (endpoint.pending >= this.max_pending) { 265 | return this.overloaded_endpoint; 266 | } 267 | // Note that if this endpoint is unhealthy, this request may fail, or it may work and then set node healthy. 268 | // Either way, if user requested a specific endpoint, they need that one, even if it's broken. 269 | return endpoint; 270 | } 271 | 272 | for (i = 0; i < endpoints_len; i++) { 273 | endpoint_pos = (endpoint_pos + 1) % endpoints_len; 274 | endpoint = this.endpoints[endpoint_pos]; 275 | if (endpoint.ready()) { 276 | return endpoint; // idle keepalive socket 277 | } else if (endpoint.healthy && endpoint.pending < min_pending_level) { 278 | min_pending_level = endpoint.pending; 279 | min_pending_endpoint = endpoint; 280 | } 281 | total_pending += endpoint.pending; 282 | } 283 | 284 | // fail request immediately if the pool is too busy 285 | if (total_pending >= this.max_pending && !options.override_pending) { 286 | return this.overloaded_endpoint; 287 | } 288 | 289 | if (min_pending_endpoint) { 290 | return min_pending_endpoint; 291 | } 292 | 293 | // if we made it this far, none of the endpoints were healthy 294 | return this.unhealthy_endpoint; 295 | }; 296 | Pool.prototype.get_node = Pool.prototype.get_endpoint; 297 | Pool.prototype.getNode = Pool.prototype.get_endpoint; 298 | 299 | Pool.prototype.pending = function pending() { 300 | var count = 0; 301 | var endpoints = this.endpoints; 302 | for (var i = 0; i < endpoints.length; i++) { 303 | count += endpoints[i].pending; 304 | } 305 | return count; 306 | }; 307 | 308 | Pool.prototype.rate = function rate() { 309 | var count = 0; 310 | var endpoints = this.endpoints; 311 | for (var i = 0; i < endpoints.length; i++) { 312 | count += endpoints[i].request_rate; 313 | } 314 | return count; 315 | }; 316 | 317 | Pool.prototype.request_count = function request_count() { 318 | var count = 0; 319 | var endpoints = this.endpoints; 320 | for (var i = 0; i < endpoints.length; i++) { 321 | count += endpoints[i].request_count; 322 | } 323 | return count; 324 | }; 325 | 326 | Pool.prototype.close = function close() { 327 | var endpoints = this.endpoints; 328 | for (var i = 0; i < endpoints.length; i++) { 329 | endpoints[i].close(); 330 | } 331 | }; 332 | 333 | Pool.prototype.valid_host_port = function valid_host_port(host_port) { 334 | var ip_port = host_port.split(":"); 335 | var ip = ip_port[0]; 336 | var port = +ip_port[1]; 337 | if (port > 0 && port < 65536) { 338 | return ip_port; 339 | } 340 | return null; 341 | }; 342 | 343 | // Dynamic membership 344 | Pool.prototype.add_endpoint = function add_endpoint(host_port) { 345 | var ip_port = this.valid_host_port(host_port); 346 | if (!ip_port) { 347 | return null; 348 | } 349 | 350 | var hasHostPort = this.all_hostports.indexOf(host_port) >= 0; 351 | if (!hasHostPort) { 352 | this.all_hostports.push(host_port); 353 | } 354 | 355 | var maxSizeIsRelevant = this.max_pool_size && 356 | this.max_pool_size < this.all_hostports.length; 357 | 358 | // if using maxSize, probabilistically add the new host port to the 359 | // active pool endpoints 360 | if (maxSizeIsRelevant) { 361 | var r = Math.floor(Math.random() * this.all_hostports.length); 362 | // note: the + 1 is a significant bias correction for adding then removing 363 | if (r < this.max_pool_size + 1) { 364 | this.add_pool_endpoint(host_port); 365 | this.adjust_pool_size(); 366 | } 367 | return null; 368 | } 369 | 370 | this.add_pool_endpoint(host_port); 371 | }; 372 | 373 | Pool.prototype.add_pool_endpoint = function add_pool_endpoint(host_port, ip_port) { 374 | ip_port = ip_port || this.valid_host_port(host_port); 375 | if (!ip_port) { 376 | return; 377 | } 378 | 379 | // check already added 380 | if (this.endpoints_by_name[host_port]) { 381 | return; 382 | } 383 | var ip = ip_port[0]; 384 | var port = +ip_port[1]; 385 | var endpoint = new GO.PoolEndpoint(this.http, ip, port, this.options); 386 | endpoint.on("health", this.endpoint_health_changed.bind(this)); 387 | endpoint.on("timeout", this.endpoint_timed_out.bind(this)); 388 | this.endpoints.push(endpoint); 389 | this.length++; 390 | this.endpoints_by_name[host_port] = endpoint; 391 | }; 392 | 393 | Pool.prototype.remove_endpoint = function rm_endpoint(host_port) { 394 | for (var j = 0; j < this.all_hostports.length; j++) { 395 | if (this.all_hostports[j] === host_port) { 396 | this.all_hostports.splice(j, 1); 397 | } 398 | } 399 | 400 | this.remove_pool_endpoint(host_port); 401 | this.adjust_pool_size(); 402 | }; 403 | 404 | Pool.prototype.remove_pool_endpoint = function rm_pool_endpoint(host_port) { 405 | var endpoint = this.endpoints_by_name[host_port]; 406 | if (!endpoint) { return; } 407 | 408 | delete this.endpoints_by_name[host_port]; 409 | endpoint.close(); 410 | this.length--; 411 | var endpoints = this.endpoints; 412 | for (var i = 0; i < endpoints.length; i++) { 413 | if (endpoints[i] === endpoint) { 414 | endpoints.splice(i, 1); 415 | return; 416 | } 417 | } 418 | }; 419 | 420 | // adjusts the pool size within max_pool_size boundary if necessary 421 | // after we add/remove a discrete element from the pool, this adjustment must 422 | // carefully select a random element to remove/add so that connections are distributed 423 | // evenly across the cluster 424 | Pool.prototype.adjust_pool_size = function adjust_pool_size() { 425 | // length can only be out of sync with all hostports if there's a max_pool_size 426 | if (!this.max_pool_size) { 427 | return; 428 | } 429 | var expected_size = Math.min(this.max_pool_size, this.all_hostports.length); 430 | 431 | if (expected_size === this.length) { 432 | return; 433 | } 434 | 435 | while (this.length !== expected_size) { 436 | var idx; 437 | if (this.length < expected_size) { 438 | // add a random host from the all host_ports 439 | var unused = this.unused_hostports(); 440 | idx = Math.floor(Math.random() * unused.length); 441 | var randHostPort = unused[idx]; 442 | if (!this.endpoints_by_name[randHostPort]) { 443 | this.add_pool_endpoint(randHostPort); 444 | } 445 | } else if (this.length > expected_size) { 446 | idx = Math.floor(Math.random() * this.length); 447 | var randEndpoint = this.endpoints[idx]; 448 | this.remove_pool_endpoint(randEndpoint.ip + ':' + randEndpoint.port); 449 | } 450 | } 451 | }; 452 | 453 | Pool.prototype.unused_hostports = function unused_hostports() { 454 | var unused = new Array(this.all_hostports.length - this.length); 455 | var i = 0; 456 | 457 | for (var j = 0; j < this.all_hostports.length; j++) { 458 | var hostport = this.all_hostports[j]; 459 | if (!this.endpoints_by_name[hostport]) { 460 | unused[i] = hostport; 461 | i++; 462 | } 463 | } 464 | return unused; 465 | }; 466 | 467 | module.exports = function init(new_GO) { 468 | GO = new_GO; 469 | return Pool; 470 | }; 471 | -------------------------------------------------------------------------------- /pool_endpoint.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var GO, // Global Object 4 | http = require("http"), 5 | inherits = require("util").inherits, 6 | EventEmitter = require("events").EventEmitter; 7 | 8 | var MAX_COUNT = Math.pow(2, 52); // if we need more than 51 bits, wrap around at 4,503,599,627,370,495. 9 | 10 | // PoolEndpoint - a backend that requests can be sent to 11 | // http: either require("http") or require("https") 12 | // ip: host ip 13 | // port: host port 14 | // options: { 15 | // ping: ping path (no ping checks) 16 | // ping_timeout: in ms (5000) 17 | // max_sockets: max concurrent open sockets (5) 18 | // timeout: default request timeout in ms (60000) 19 | // resolution: how often timeouts are checked in ms (1000) 20 | // keep_alive: use an alternate Agent that does keep-alive properly (boolean) default false 21 | // agent_ptions: {} an object for passing options directly to the Http Agent 22 | // } 23 | function PoolEndpoint(protocol, ip, port, options) { 24 | options = options || {}; 25 | 26 | this.http = protocol; 27 | this.healthy = true; 28 | 29 | this.ip = ip; 30 | this.address = ip; 31 | this.port = port; 32 | 33 | this.keep_alive = options.keep_alive || options.keepAlive; 34 | this.agent_options = options.agent_options || options.agentOptions; 35 | 36 | if (this.keep_alive) { 37 | if (protocol === http) { 38 | this.agent = new GO.KeepAliveAgent.HTTP(this.agent_options); 39 | } else { 40 | this.agent = new GO.KeepAliveAgent.HTTPS(this.agent_options); 41 | } 42 | } else { 43 | this.agent = new protocol.Agent(this.agent_options); 44 | } 45 | 46 | if (this.agent && typeof this.agent.getName === 'function') { 47 | this.name = this.agent.getName({ 48 | host: this.ip, 49 | port: this.port 50 | }); 51 | } else { 52 | this.name = this.ip + ":" + this.port; 53 | } 54 | 55 | this.agent.maxSockets = options.max_sockets || options.maxSockets || 5; 56 | 57 | this.requests = Object.create(null); 58 | this.request_count = 0; 59 | this.requests_last_check = 0; 60 | this.request_rate = 0; 61 | this.pending = 0; 62 | this.successes = 0; 63 | this.failures = 0; 64 | this.filtered = 0; 65 | 66 | this.timeout = (options.timeout === 0) ? 0 : options.timeout || (60 * 1000); 67 | this.resolution = (options.resolution === 0) ? 0 : options.resolution || 1000; 68 | this.timeout_enabled = this.resolution > 0 && this.timeout > 0; 69 | this.timeout_interval = null; 70 | this.bindCheckTimeouts = bindCheckTimeouts; 71 | 72 | // note that the pinger doesn't start by default, but in the future we might want to add an option for checking an endpoint before ever using it 73 | this.ping_path = options.ping; 74 | this.ping_timeout = options.ping_timeout || 5000; 75 | this.pinger = new GO.PoolPinger(this); 76 | 77 | var self = this; 78 | function bindCheckTimeouts() { 79 | self.check_timeouts(); 80 | } 81 | } 82 | inherits(PoolEndpoint, EventEmitter); 83 | 84 | PoolEndpoint.prototype.close = function () { 85 | clearInterval(this.timeout_interval); 86 | // No more ping-ing. 87 | this.ping_path = null; 88 | var request_ids = Object.keys(this.requests); 89 | for (var i = 0; i < request_ids.length; i++) { 90 | var req_id = request_ids[i]; 91 | this.requests[req_id].on_aborted(); 92 | this.delete_request(req_id); 93 | } 94 | 95 | if (this.agent && typeof(this.agent.destroy) === 'function') { 96 | this.agent.destroy(); 97 | } 98 | }; 99 | 100 | // options: { 101 | // timeout: request timeout in ms (this.timeout) 102 | // encoding: response body encoding (utf8) 103 | // data: string or buffer 104 | // } 105 | PoolEndpoint.prototype.request = function (options, callback) { 106 | var has_retry = !!options.retry_filter, 107 | req = new GO.PoolEndpointRequest(this, options, callback); 108 | 109 | this.update_pending(); 110 | this.requests[req.id] = req; 111 | req.start(); 112 | 113 | if (this.timeout_enabled && !this.timeout_interval) { 114 | this.timeout_interval = setInterval( 115 | this.bindCheckTimeouts, 116 | this.resolution 117 | ); 118 | } 119 | 120 | // If you want to retry, you can't stream. 121 | if (has_retry) { 122 | return; 123 | } 124 | return req; 125 | }; 126 | 127 | PoolEndpoint.prototype.ready = function () { 128 | if (! this.healthy) { 129 | return false; // unhealthy endpoints are never ready 130 | } 131 | if (this.keep_alive) { 132 | // if we are doing keep_alive and we have more sockets than active requests, we are ready 133 | if (this.agent.sockets[this.name] && this.agent.sockets[this.name].length > this.pending) { 134 | return true; 135 | } else { 136 | return false; 137 | } 138 | } 139 | // we are ready if we currently have nothing to do 140 | return this.pending === 0; 141 | }; 142 | 143 | PoolEndpoint.prototype.stats = function () { 144 | var socket_keys = Object.keys(this.agent.sockets); 145 | var request_counts = []; 146 | for (var i = 0; i < socket_keys.length; i++) { 147 | var name = socket_keys[i]; 148 | var s = this.agent.sockets[name] || []; 149 | for (var j = 0; j < s.length; j++) { 150 | request_counts.push(s[j]._request_count || 1); 151 | } 152 | } 153 | return { 154 | name: this.name, 155 | request_count: this.request_count, 156 | request_rate: this.request_rate, 157 | pending: this.pending, 158 | successes: this.successes, 159 | failures: this.failures, 160 | filtered: this.filtered, 161 | healthy: this.healthy, 162 | socket_count: this.agent.sockets[this.name] ? this.agent.sockets[this.name].length : 0, 163 | socket_request_counts: request_counts 164 | }; 165 | }; 166 | 167 | PoolEndpoint.prototype.check_timeouts = function () { 168 | if (this.pending === 0) { 169 | clearInterval(this.timeout_interval); 170 | this.timeout_interval = null; 171 | return; 172 | } 173 | 174 | var requests = this.requests; 175 | var now = Date.now(); // only run Date.now() once per check interval 176 | var delete_array = []; 177 | for (var req_id in requests) { 178 | var request = requests[req_id]; 179 | var expire_time = now - request.options.timeout; 180 | 181 | if (request.last_touched <= expire_time) { 182 | if (request.options.path !== this.ping_path) { 183 | this.emit("timeout", request); 184 | } 185 | request.timed_out = true; 186 | request.out_request.abort(); 187 | delete_array.push(req_id); 188 | } 189 | } 190 | for (var i = 0; i < delete_array.length; i++) { 191 | this.delete_request(delete_array[i]); 192 | } 193 | this.request_rate = this.request_count - this.requests_last_check; 194 | this.requests_last_check = this.request_count; 195 | }; 196 | 197 | PoolEndpoint.prototype.reset_counters = function () { 198 | this.requests_last_check = this.request_rate - this.pending; 199 | this.request_count = this.pending; 200 | this.successes = 0; 201 | this.failures = 0; 202 | this.filtered = 0; 203 | }; 204 | 205 | PoolEndpoint.prototype.update_pending = function () { 206 | this.pending = this.request_count - (this.successes + this.failures + this.filtered); 207 | if (this.request_count === MAX_COUNT) { 208 | this.reset_counters(); 209 | } 210 | }; 211 | 212 | PoolEndpoint.prototype.complete = function (err, request, response, body) { 213 | this.delete_request(request.id); 214 | this.update_pending(); 215 | request.done(err, response, body); 216 | }; 217 | 218 | PoolEndpoint.prototype.request_succeeded = function (request, response, body) { 219 | this.successes++; 220 | this.complete(null, request, response, body); 221 | }; 222 | 223 | PoolEndpoint.prototype.request_failed = function (err, request) { 224 | this.failures++; 225 | if (!request.destroyed) { 226 | this.set_healthy(false); 227 | } 228 | this.complete(err, request); 229 | }; 230 | 231 | PoolEndpoint.prototype.filter_rejected = function (err, request) { 232 | this.filtered++; 233 | this.complete(err, request); 234 | }; 235 | 236 | PoolEndpoint.prototype.busyness = function () { 237 | return this.pending; 238 | }; 239 | 240 | PoolEndpoint.prototype.set_healthy = function (new_state) { 241 | if (! this.ping_path) { 242 | return; // an endpoint with no pingPath can never be made unhealthy 243 | } 244 | if (! new_state) { 245 | this.pinger.start(); 246 | } 247 | if (this.healthy !== new_state) { 248 | this.healthy = new_state; 249 | this.emit("health", this); 250 | } 251 | }; 252 | 253 | PoolEndpoint.prototype.setHealthy = PoolEndpoint.prototype.set_healthy; 254 | 255 | PoolEndpoint.prototype.delete_request = function (id) { 256 | delete this.requests[id]; 257 | }; 258 | 259 | module.exports = function init(new_global) { 260 | GO = new_global; 261 | 262 | return PoolEndpoint; 263 | }; 264 | -------------------------------------------------------------------------------- /pool_endpoint_request.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var Stream = require("stream"), 4 | inherits = require("util").inherits, 5 | GO; 6 | 7 | function noop() { 8 | return false; 9 | } 10 | 11 | function PoolEndpointRequest(endpoint, options, callback) { 12 | options.host = endpoint.ip; 13 | options.port = endpoint.port; 14 | options.retry_filter = options.retry_filter || noop; 15 | options.timeout = options.timeout || endpoint.timeout; 16 | options.headers = cleanHeaders(options.headers); 17 | if (options.agent !== false) { 18 | options.agent = endpoint.agent; 19 | } 20 | if (typeof options.encoding === "string") { 21 | options.encoding = options.encoding || "utf8"; 22 | } 23 | 24 | this.id = endpoint.request_count++; 25 | this.endpoint = endpoint; 26 | this.options = options; 27 | this.callback = callback || noop; // note that this.endpoint reaches in and calls this 28 | this.last_touched = Date.now(); 29 | this.req_end = null; 30 | this.res_start = null; 31 | 32 | this.response = null; 33 | this.writable = true; 34 | this.readable = true; 35 | 36 | this.buffer_body = options.buffer_body !== false; 37 | this.body_chunks = []; 38 | this.body_length = 0; 39 | 40 | this.timed_out = false; 41 | 42 | // message_router uses these next two, because of specialized backpressure logic 43 | this.buffered_writes = 0; 44 | this.buffered_writes_bytes = 0; 45 | this.state = "init"; 46 | this.destroyed = false; 47 | 48 | this.out_request = null; 49 | } 50 | 51 | inherits(PoolEndpointRequest, Stream); 52 | 53 | PoolEndpointRequest.prototype.start = function () { 54 | var self = this; 55 | this.out_request = this.endpoint.http.request(this.options); 56 | this.out_request.on("response", function (response) { self.on_response(response); }); 57 | this.out_request.on("error", function (err) { self.on_error(err); }); 58 | this.out_request.on("drain", function () { self.on_drain(); }); 59 | this.out_request.on("abort", function onAbort(err) { 60 | self.on_error(new Error('lb_pool: request aborted')); 61 | }); 62 | 63 | var data = this.options.data; 64 | if (!data && this.options.end === false) { 65 | return; 66 | } 67 | if (!data) { 68 | return this.end(); 69 | } 70 | if (data instanceof Stream) { 71 | return data.pipe(this); 72 | } 73 | 74 | this.out_request.setHeader("Content-Length", Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); 75 | this.end(data); 76 | }; 77 | 78 | PoolEndpointRequest.prototype.on_response = function (response) { 79 | var self = this; 80 | this.response = response; 81 | this.state = "res_start"; 82 | this.res_start = Date.now(); 83 | 84 | response.on("data", function (chunk) { self.on_data(chunk); }); 85 | response.on("end", function () { self.on_end(); }); 86 | response.on("aborted", function () { self.on_aborted(); }); 87 | }; 88 | 89 | PoolEndpointRequest.prototype.on_error = function (err) { 90 | if (!this.callback) { 91 | return; 92 | } 93 | 94 | if (this.timed_out) { 95 | return this.on_request_timeout(); 96 | } 97 | 98 | this.writable = false; 99 | this.readable = false; 100 | this.state = "error"; 101 | 102 | var msg = this.endpoint.name + " error: " + (this.timed_out ? "request timed out" : err.message); 103 | this.endpoint.request_failed({ 104 | reason: err.message, 105 | attempt: this.get_attempt_info(), 106 | message: msg, 107 | }, this); 108 | }; 109 | 110 | PoolEndpointRequest.prototype.on_drain = function () { 111 | this.last_touched = Date.now(); 112 | this.buffered_writes = 0; 113 | this.buffered_writes_bytes = 0; 114 | this.emit("drain"); 115 | }; 116 | 117 | PoolEndpointRequest.prototype.on_data = function (chunk) { 118 | this.last_touched = Date.now(); 119 | if (this.buffer_body) { 120 | this.body_chunks.push(chunk); 121 | this.body_length += chunk.length; 122 | } 123 | this.state = "res_read"; 124 | this.emit("data", chunk); 125 | }; 126 | 127 | PoolEndpointRequest.prototype.on_end = function () { 128 | if (!this.callback) { 129 | return; 130 | } 131 | 132 | var self = this; 133 | this.readable = false; 134 | 135 | if (this.callback === null) { return; } 136 | if (this.timed_out) { return this.on_response_timeout(); } 137 | 138 | this.state = "res_end"; 139 | this.emit("end"); 140 | 141 | var body; 142 | if (this.buffer_body) { 143 | var body_buf = Buffer.concat(this.body_chunks, this.body_length); 144 | if (this.options.encoding !== null) { 145 | body = body_buf.toString(this.options.encoding); 146 | } else { 147 | body = body_buf; 148 | } 149 | this.body_chunks.length = 0; 150 | } 151 | 152 | var delay = this.options.retry_filter(this.options, this.response, body); 153 | if (delay !== false) { // delay may be 0 154 | return this.endpoint.filter_rejected({ 155 | delay: delay, 156 | reason: "filter", 157 | attempt: this.get_attempt_info(), 158 | message: self.endpoint.name + " error: rejected by filter" 159 | }, this); 160 | } 161 | this.endpoint.request_succeeded(this, this.response, body); 162 | }; 163 | 164 | PoolEndpointRequest.prototype.on_aborted = function () { 165 | if (!this.callback) { 166 | return; 167 | } 168 | 169 | if (this.timed_out) { 170 | return this.on_response_timeout(); 171 | } 172 | 173 | var msg = this.endpoint.name + " error: connection aborted"; 174 | this.state = "res_aborted"; 175 | this.endpoint.request_failed({ 176 | reason: "aborted", 177 | attempt: this.get_attempt_info(), 178 | message: msg, 179 | }, this); 180 | }; 181 | 182 | // timeout occurred before receiving anything from server 183 | PoolEndpointRequest.prototype.on_request_timeout = function () { 184 | if (!this.callback) { 185 | return; 186 | } 187 | 188 | var msg = this.endpoint.name + " error: request timed out"; 189 | this.state = "req_timeout"; 190 | this.endpoint.request_failed({ 191 | reason: "timed_out", 192 | attempt: this.get_attempt_info(), 193 | message: msg, 194 | }, this); 195 | }; 196 | 197 | // timeout occurred after receiving partial response from server 198 | PoolEndpointRequest.prototype.on_response_timeout = function () { 199 | if (!this.callback) { 200 | return; 201 | } 202 | 203 | var msg = this.endpoint.name + " error: response timed out"; 204 | this.state = "res_timeout"; 205 | this.endpoint.request_failed({ 206 | reason: "timed_out", 207 | attempt: this.get_attempt_info(), 208 | message: msg, 209 | }, this); 210 | }; 211 | 212 | PoolEndpointRequest.prototype.write = function (buf) { 213 | // Prevent memory leak in node 0.8.16 214 | if (this.out_request.socket && this.out_request.socket.destroyed) { 215 | this.out_request.emit("close"); 216 | return false; 217 | } 218 | var success = this.out_request.write(buf); 219 | if (success) { 220 | this.last_touched = Date.now(); 221 | this.buffered_writes = 0; 222 | this.buffered_writes_bytes = 0; 223 | this.state = "req_write"; 224 | } else { 225 | this.buffered_writes += 1; 226 | this.buffered_writes_bytes += buf.length; 227 | this.state = "req_write_buffer"; 228 | } 229 | return success; 230 | }; 231 | 232 | PoolEndpointRequest.prototype.end = function (buf) { 233 | this.req_end = Date.now(); 234 | this.last_touched = Date.now(); 235 | this.writable = false; 236 | this.state = "req_end"; 237 | return this.out_request.end(buf); 238 | }; 239 | 240 | PoolEndpointRequest.prototype.destroy = function () { 241 | this.writable = false; 242 | this.readable = false; 243 | this.state = "abort"; 244 | this.destroyed = true; 245 | // Don't call destroy cause that will throw an exception when req.socket doesnt exist. 246 | this.out_request.abort(); 247 | }; 248 | 249 | PoolEndpointRequest.prototype.abort = PoolEndpointRequest.prototype.destroy; 250 | 251 | PoolEndpointRequest.prototype.done = function (err, response, body) { 252 | var start = this.req_end || Date.now(), 253 | end = this.res_start || Date.now(); 254 | this.callback(err, response, body, end - start); 255 | this.emit("done", err, response, body, this); 256 | this.callback = null; 257 | }; 258 | 259 | PoolEndpointRequest.prototype.get_attempt_info = get_attempt_info; 260 | function get_attempt_info() { 261 | return new PoolEndpointRequestAttempt(this); 262 | } 263 | 264 | function PoolEndpointRequestAttempt(poolEndpointRequest) { 265 | var options = poolEndpointRequest.options || {}; 266 | var endpoint = poolEndpointRequest.endpoint || {}; 267 | 268 | this.endpoint_address = endpoint.address; 269 | this.endpoint_failures = endpoint.failures; 270 | this.endpoint_filtered = endpoint.filtered; 271 | this.endpoint_healthy = endpoint.healthy; 272 | this.endpoint_ip = endpoint.ip; 273 | this.endpoint_keepalive = endpoint.keep_alive; 274 | this.endpoint_name = endpoint.name; 275 | this.endpoint_pending = endpoint.pending; 276 | this.endpoint_port = endpoint.port; 277 | this.endpoint_request_count = endpoint.request_count; 278 | this.endpoint_request_last_check = endpoint.requests_last_check; 279 | this.endpoint_request_rate = endpoint.request_rate; 280 | this.endpoint_resolution = endpoint.resolution; 281 | this.endpoint_successes = endpoint.successes; 282 | this.endpoint_timeout = endpoint.timeout; 283 | this.options_headers = options.headers; 284 | this.options_host = options.host; 285 | this.options_method = options.method; 286 | this.options_path = options.path; 287 | this.options_port = options.port; 288 | this.options_retry_delay = options.retry_delay; 289 | this.options_reused = options.reused; 290 | this.options_success = options.success; 291 | this.options_timeout = options.timeout; 292 | this.request_last_touched = poolEndpointRequest.last_touched; 293 | this.request_readable = poolEndpointRequest.readable; 294 | this.request_req_end = poolEndpointRequest.req_end; 295 | this.request_res_start = poolEndpointRequest.res_start; 296 | this.request_writable = poolEndpointRequest.writable; 297 | } 298 | 299 | function cleanHeaders(headers) { 300 | if (!headers) { 301 | return {}; 302 | } 303 | 304 | var newHeaders = {}; 305 | var headerNames = Object.keys(headers); 306 | 307 | for (var i = 0; i < headerNames.length; i++) { 308 | var headerName = headerNames[i]; 309 | var headerValue = headers[headerName]; 310 | if (headerValue != null) { 311 | newHeaders[headerName] = String(headerValue); 312 | } 313 | } 314 | 315 | return newHeaders; 316 | } 317 | 318 | module.exports = function init(new_GO) { 319 | GO = new_GO; 320 | 321 | return PoolEndpointRequest; 322 | }; 323 | -------------------------------------------------------------------------------- /pool_pinger.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var GO; 4 | 5 | function PoolPinger(pool_endpoint) { 6 | this.pool_endpoint = pool_endpoint; 7 | this.running = false; 8 | this.attempts = 0; 9 | this.ping_timeout = pool_endpoint.ping_timeout; 10 | this.out_req = null; 11 | this.req_timer = null; 12 | } 13 | 14 | PoolPinger.prototype.start = function () { 15 | if (! this.pool_endpoint.ping_path) { 16 | return; 17 | } 18 | 19 | if (! this.running) { 20 | this.running = true; 21 | this.attempts = 0; 22 | this.ping(); 23 | } 24 | }; 25 | 26 | PoolPinger.prototype.ping = function () { 27 | if (this.req_timer) { 28 | clearTimeout(this.req_timer); 29 | } 30 | if (this.attempts > 0) { 31 | setTimeout(this.make_request.bind(this), this.backoff()); 32 | } else { 33 | this.make_request(); 34 | } 35 | }; 36 | 37 | // Make a request to the ping_path using bare node and no agent. This way we won't create a new socket on the 38 | // real agent and thus make a newly revived node be prefered. 39 | PoolPinger.prototype.make_request = function () { 40 | var self = this; 41 | 42 | if (! this.pool_endpoint.ping_path) { 43 | return; 44 | } 45 | 46 | this.req_timer = setTimeout(function () { 47 | self.on_timeout(); 48 | }, this.ping_timeout); 49 | 50 | this.out_req = this.pool_endpoint.http.get({ 51 | host: this.pool_endpoint.ip, 52 | port: this.pool_endpoint.port, 53 | agent: false, 54 | path: this.pool_endpoint.ping_path 55 | }); 56 | this.out_req.on("response", function (res) { 57 | self.out_req = null; 58 | self.on_response(res); 59 | }); 60 | this.out_req.on("error", function () { 61 | self.out_req = null; 62 | self.attempts++; 63 | self.ping(); 64 | }); 65 | }; 66 | 67 | PoolPinger.prototype.on_response = function (res) { 68 | if (res.statusCode === 200) { 69 | clearTimeout(this.req_timer); 70 | this.pool_endpoint.set_healthy(true); 71 | this.running = false; 72 | } else { 73 | this.attempts++; 74 | this.ping(); 75 | } 76 | }; 77 | 78 | PoolPinger.prototype.on_timeout = function () { 79 | this.req_timer = null; 80 | this.out_req.abort(); 81 | this.out_req = null; 82 | // calling abort() will run the "error" listener which will retry 83 | }; 84 | 85 | // Add some fun random variance to the delay until we get to 20 seconds, then keep retrying at 20. 86 | PoolPinger.prototype.backoff = function () { 87 | return Math.min(Math.floor(Math.random() * Math.pow(2, this.attempts) + 10), 20000); 88 | }; 89 | 90 | module.exports = function init(new_GO) { 91 | GO = new_GO; 92 | 93 | return PoolPinger; 94 | }; 95 | -------------------------------------------------------------------------------- /pool_request_set.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var Stream = require("stream"); 4 | 5 | // PoolRequestSet - an object to track server requests and handle retries 6 | // 7 | // pool: a pool of endpoints 8 | // options: { 9 | // attempts: number of tries 10 | // maxHangups: number of 'socket hang ups' before giving up (2) 11 | // timeout: request timeout in ms 12 | // maxAborts: number of 'aborted' before giving up (2) 13 | // retryDelay: minimum ms to wait before first retry using exponential backoff (20) 14 | // } 15 | // callback: function (err, response, body) {} 16 | 17 | function PoolRequestSet(pool, options, callback) { 18 | this.options = options || {}; 19 | this.pool = pool; 20 | this.callback = callback; 21 | 22 | if (this.options.data instanceof Stream || this.options.end === false) { 23 | this.max_attempts = 1; 24 | } else { 25 | this.max_attempts = options.max_attempts || Math.min(pool.options.max_retries + 1, Math.max(pool.length, 2)); 26 | } 27 | this.attempts_remaining = this.max_attempts; 28 | 29 | this.max_timeouts = options.max_timeouts || 1; // no retries on timeouts by default 30 | this.timeouts = 0; 31 | 32 | this.max_hangups = options.max_hangups || 2; 33 | this.hangups = 0; 34 | 35 | this.max_aborts = options.max_aborts || 2; 36 | this.aborts = 0; 37 | this.duration = null; 38 | 39 | if (!options.retry_delay && options.retry_delay !== 0) { 40 | options.retry_delay = 20; 41 | } 42 | this.delay = options.retry_delay; 43 | } 44 | 45 | PoolRequestSet.prototype.handle_response = function (err, response, body) { 46 | var delay; 47 | 48 | this.attempts_remaining--; 49 | 50 | if (err) { 51 | delay = Math.round(Math.random() * Math.pow(2, this.max_attempts - this.attempts_remaining) * this.delay); 52 | err.delay = delay; // stash delay here so "retrying" listeners can understand the delay 53 | 54 | if (err.reason === "socket hang up") { 55 | this.hangups++; 56 | } else if (err.reason === "aborted") { 57 | this.aborts++; 58 | } else if (err.reason === "timed_out") { 59 | this.timeouts++; 60 | } 61 | 62 | if (this.attempts_remaining > 0 && err.reason !== "full" && err.reason !== "unhealthy" && 63 | this.hangups < this.max_hangups && this.aborts < this.max_aborts && this.timeouts < this.max_timeouts) { 64 | this.pool.on_retry(err); 65 | if (delay > 0) { 66 | setTimeout(this.do_request.bind(this), delay); 67 | } else { 68 | this.do_request(); 69 | } 70 | return; 71 | } 72 | } 73 | if (this.callback) { 74 | this.callback(err, response, body); 75 | this.callback = null; 76 | } 77 | }; 78 | 79 | PoolRequestSet.prototype.do_request = function () { 80 | var endpoint = this.pool.get_endpoint(this.options), 81 | self = this; 82 | 83 | return endpoint.request(this.options, function (err, res, body, duration) { 84 | self.duration = duration; 85 | self.handle_response(err, res, body); 86 | }); 87 | }; 88 | 89 | module.exports = function init() { 90 | return PoolRequestSet; 91 | }; 92 | -------------------------------------------------------------------------------- /test/endpoint_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var assert = require("assert"); 4 | var http = require("http"); 5 | var https = require("https"); 6 | 7 | var noop = function () {}; 8 | 9 | var RV = {}; 10 | var PoolEndpoint; 11 | 12 | RV.PoolPinger = require("../pool_pinger")(RV); 13 | RV.KeepAliveAgent = require("../keep_alive_agent")(RV); 14 | RV.PoolEndpoint = require("../pool_endpoint")(RV); 15 | RV.PoolEndpointRequest = require("../pool_endpoint_request")(RV); 16 | PoolEndpoint = RV.PoolEndpoint; 17 | 18 | describe("PoolEndpoint", function () { 19 | it("passes nothing to the Agent constructor when no agentOptions are given", function () { 20 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, { bogus: true }); 21 | assert.equal(e.agent.options.bogus, undefined); 22 | }); 23 | 24 | it("passes agentOptions to the underlying Agent (no keep-alive)", function () { 25 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, { agentOptions: { cert: "foo", key: "bar"}}); 26 | assert.equal(e.agent.options.cert, "foo"); 27 | assert.equal(e.agent.options.key, "bar"); 28 | }); 29 | 30 | it("passes agentOptions to the underlying Agent (keep-alive)", function () { 31 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {keepAlive: true, agentOptions: { cert: "foo", key: "bar"}}); 32 | assert.equal(e.agent.options.cert, "foo"); 33 | assert.equal(e.agent.options.key, "bar"); 34 | }); 35 | 36 | it("passes agentOptions to the underlying Agent (keep-alive secure)", function () { 37 | var e = new PoolEndpoint(https, "127.0.0.1", 6969, {keepAlive: true, agentOptions: { cert: "foo", key: "bar"}}); 38 | assert.equal(e.agent.options.cert, "foo"); 39 | assert.equal(e.agent.options.key, "bar"); 40 | }); 41 | 42 | describe("request()", function () { 43 | it("sends Content-Length when data is a string", function (done) { 44 | var port = 6970; 45 | var s = http.createServer(function (req, res) { 46 | assert.equal(req.headers["content-length"], 4); 47 | res.end("foo"); 48 | s.close(); 49 | done(); 50 | }); 51 | s.on("listening", function () { 52 | var e = new PoolEndpoint(http, "127.0.0.1", port); 53 | e.request({path: "/foo", method: "PUT", data: "ƒoo"}, noop); 54 | }); 55 | s.listen(port); 56 | }); 57 | 58 | it("sends Content-Length when data is a buffer", function (done) { 59 | var s = http.createServer(function (req, res) { 60 | assert.equal(req.headers["content-length"], 4); 61 | res.end("foo"); 62 | s.close(); 63 | done(); 64 | }); 65 | s.on("listening", function () { 66 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 67 | e.request({path: "/foo", method: "PUT", data: new Buffer("ƒoo")}, noop); 68 | }); 69 | s.listen(6969); 70 | }); 71 | 72 | it("times out and returns an error when the server fails to respond in time", function (done) { 73 | var s = http.createServer(function (req, res) { 74 | setTimeout(function () { 75 | res.end("foo"); 76 | }, 30); 77 | }); 78 | s.on("listening", function () { 79 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 80 | var error; 81 | e.request({path: "/foo", method: "GET"}, function (err, response, body) { 82 | error = err; 83 | assert.strictEqual(error.reason, "timed_out"); 84 | assert.strictEqual(/request timed out$/.test(error.message), true); 85 | assert.strictEqual(response, undefined); 86 | assert.strictEqual(body, undefined); 87 | }); 88 | setTimeout(function () { 89 | s.close(); 90 | done(); 91 | }, 40); 92 | }); 93 | s.listen(6969); 94 | }); 95 | 96 | it("times out and returns an error when the server response hasn't sent any data within the timeout", function (done) { 97 | this.timeout(0); 98 | var s = http.createServer(function (req, res) { 99 | res.writeHead(200); 100 | 101 | setTimeout(function () { 102 | res.write("foo"); 103 | }, 10); 104 | 105 | setTimeout(function () { 106 | res.write("bar"); 107 | }, 40); 108 | 109 | }); 110 | s.on("listening", function () { 111 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 15, resolution: 10}); 112 | var error; 113 | e.request({path: "/foo", method: "GET"}, function (err, response, body) { 114 | error = err; 115 | assert.strictEqual(response, undefined); 116 | assert.strictEqual(body, undefined); 117 | }); 118 | 119 | setTimeout(function () { 120 | s.close(); 121 | 122 | assert.equal(error.reason, "timed_out"); 123 | assert.equal(/timed out$/.test(error.message), true); 124 | done(); 125 | }, 60); 126 | }); 127 | s.listen(6969); 128 | }); 129 | 130 | it("emits a timeout event on timeout", function (done) { 131 | var s = http.createServer(function (req, res) { 132 | setTimeout(function () { 133 | res.end("foo"); 134 | }, 30); 135 | }); 136 | s.on("listening", function () { 137 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 138 | var fin = false; 139 | e.on("timeout", function () { 140 | fin = true; 141 | }); 142 | e.request({path: "/foo", method: "GET"}, noop); 143 | 144 | setTimeout(function () { 145 | s.close(); 146 | assert.equal(fin, true); 147 | done(); 148 | }, 60); 149 | }); 150 | s.listen(6969); 151 | }); 152 | 153 | it("removes the request from this.requests on timeout", function (done) { 154 | var s = http.createServer(function (req, res) { 155 | setTimeout(function () { 156 | res.end("foo"); 157 | }, 30); 158 | }); 159 | s.on("listening", function () { 160 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {keepAlive: true, timeout: 20, resolution: 10}); 161 | var fin = false; 162 | e.on("timeout", function () { 163 | fin = true; 164 | }); 165 | e.request({path: "/foo", method: "GET"}, noop); 166 | e.request({path: "/foo", method: "GET"}, noop); 167 | e.request({path: "/foo", method: "GET"}, noop); 168 | 169 | setTimeout(function () { 170 | assert.equal(fin, true); 171 | assert.equal(Object.keys(e.requests).length, 0); 172 | s.close(); 173 | done(); 174 | }, 100); 175 | }); 176 | s.listen(6969); 177 | }); 178 | 179 | it("stops and starts the timeout interval properly", function (done) { 180 | var s = http.createServer(function handleTestReq(req, res) { 181 | setTimeout(function () { 182 | res.end("foo"); 183 | }, 30); 184 | }); 185 | s.on("listening", function onTestServerListening() { 186 | var e = new PoolEndpoint( 187 | http, 188 | "127.0.0.1", 189 | s.address().port, 190 | { 191 | keepAlive: true, 192 | timeout: 20, 193 | resolution: 10 194 | } 195 | ); 196 | var fin = false; 197 | 198 | assert(e.timeout_interval === null); 199 | 200 | 201 | e.on("timeout", function onPoolEndpointTimeout() { 202 | fin = true; 203 | }); 204 | e.request({path: "/foo", method: "GET"}, noop); 205 | e.request({path: "/foo", method: "GET"}, noop); 206 | e.request({path: "/foo", method: "GET"}, noop); 207 | 208 | setTimeout(function afterFirstRequestsTimeout() { 209 | assert.equal(fin, true); 210 | assert.equal(Object.keys(e.requests).length, 0); 211 | 212 | // All requests have ended, interval should be stopped 213 | assert(e.timeout_interval === null); 214 | fin = false; 215 | 216 | // The timeout interval should start again 217 | e.request({path: "/foo", method: "GET"}, noop); 218 | e.request({path: "/foo", method: "GET"}, noop); 219 | e.request({path: "/foo", method: "GET"}, noop); 220 | assert(e.timeout_interval !== null); 221 | setTimeout(function afterSecondRequestsTimeout() { 222 | assert.equal(fin, true); 223 | assert.equal(Object.keys(e.requests).length, 0); 224 | s.close(); 225 | assert(e.timeout_interval === null); 226 | done(); 227 | }, 100); 228 | }, 100); 229 | }); 230 | 231 | s.listen(0); 232 | }); 233 | 234 | it("removes the request from this.requests on error", function (done) { 235 | var s = http.createServer(function (req, res) { 236 | setTimeout(function () { 237 | res.end("foo"); 238 | }, 30); 239 | }); 240 | s.on("listening", function () { 241 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 242 | var error; 243 | e.request({path: "/foo", method: "GET"}, function (err, response, body) { 244 | error = err; 245 | assert.strictEqual(response, undefined); 246 | assert.strictEqual(body, undefined); 247 | }); 248 | 249 | setTimeout(function () { 250 | s.close(); 251 | assert.equal(error.reason, "timed_out"); 252 | assert.equal(/request timed out$/.test(error.message), true); 253 | assert.equal(Object.keys(e.requests).length, 0); 254 | done(); 255 | }, 50); 256 | }); 257 | s.listen(6969); 258 | }); 259 | 260 | it("removes the request from this.requests on aborted", function (done) { 261 | var s = http.createServer(function (req, res) { 262 | res.writeHead(200); 263 | res.write("foo"); 264 | setTimeout(function () { 265 | req.connection.destroy(); 266 | }, 10); 267 | }); 268 | s.on("listening", function () { 269 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 270 | var error; 271 | e.request({path: "/foo", method: "GET"}, function (err, response, body) { 272 | error = err; 273 | assert.strictEqual(response, undefined); 274 | assert.strictEqual(body, undefined); 275 | }); 276 | 277 | setTimeout(function () { 278 | s.close(); 279 | assert.equal(error.reason, "aborted"); 280 | assert.equal(Object.keys(e.requests).length, 0); 281 | done(); 282 | }, 50); 283 | }); 284 | s.listen(6969); 285 | }); 286 | 287 | it("removes the request from this.requests on success", function (done) { 288 | var s = http.createServer(function (req, res) { 289 | setTimeout(function () { 290 | res.end("foo"); 291 | }, 10); 292 | }); 293 | s.on("listening", function () { 294 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 295 | var error; 296 | e.request({path: "/foo", method: "GET"}, function (err, response, body) { 297 | error = err; 298 | assert.strictEqual(response.statusCode, 200); 299 | assert.strictEqual(body, "foo"); 300 | }); 301 | 302 | setTimeout(function () { 303 | s.close(); 304 | assert.equal(error, null); 305 | assert.equal(Object.keys(e.requests).length, 0); 306 | done(); 307 | }, 50); 308 | }); 309 | s.listen(6969); 310 | }); 311 | 312 | it("returns the whole body to the callback", function (done) { 313 | var s = http.createServer(function (req, res) { 314 | res.write("foo"); 315 | setTimeout(function () { 316 | res.end("bar"); 317 | }, 10); 318 | }); 319 | s.on("listening", function () { 320 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10}); 321 | var body; 322 | e.request({path: "/foo", method: "GET"}, function (err, response, b) { 323 | body = b; 324 | }); 325 | 326 | setTimeout(function () { 327 | s.close(); 328 | assert.equal(body, "foobar"); 329 | done(); 330 | }, 50); 331 | }); 332 | s.listen(6969); 333 | }); 334 | 335 | it("buffers the response when callback has 3 arguments and options.stream is not true", function (done) { 336 | var s = http.createServer(function (req, res) { 337 | res.end("foo"); 338 | }); 339 | s.on("listening", function () { 340 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {timeout: 20, resolution: 10, max_pending: 1}); 341 | e.request({path: "/ping", method: "GET"}, function (err, response, body) { 342 | assert.equal(response.statusCode, 200); 343 | assert.equal(response.complete, true); 344 | assert.equal(body, "foo"); 345 | s.close(); 346 | done(); 347 | }); 348 | }); 349 | s.listen(6969); 350 | }); 351 | }); 352 | 353 | describe("update_pending()", function () { 354 | it("maintains the correct pending count when requestCount 'overflows'", function () { 355 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 356 | e.successes = (Math.pow(2, 52) / 2) - 250; 357 | e.failures = (Math.pow(2, 52) / 2) - 251; 358 | e.filtered = 1; 359 | e.request_count = Math.pow(2, 52); 360 | e.update_pending(); 361 | assert.equal(e.pending, 500); 362 | assert.equal(e.request_count, 500); 363 | }); 364 | 365 | it("maintains the correct requestRate when requestCount 'overflows'", function () { 366 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 367 | e.pending = 500; 368 | e.request_rate = 500; 369 | e.request_count = Math.pow(2, 52); 370 | e.requests_last_check = e.request_count - 500; 371 | e.reset_counters(); 372 | assert.equal(e.request_count - e.requests_last_check, e.request_rate); 373 | }); 374 | }); 375 | 376 | describe("resetCounters()", function () { 377 | it("sets successes, failures and filtered to 0", function () { 378 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 379 | e.successes = (Math.pow(2, 52) / 2) - 250; 380 | e.failures = (Math.pow(2, 52) / 2) - 251; 381 | e.filtered = 1; 382 | e.request_count = Math.pow(2, 52); 383 | e.reset_counters(); 384 | assert.equal(e.successes, 0); 385 | assert.equal(e.failures, 0); 386 | assert.equal(e.filtered, 0); 387 | }); 388 | 389 | it("sets requestCount = pending", function () { 390 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 391 | e.pending = 500; 392 | e.request_rate = 400; 393 | e.request_count = Math.pow(2, 52); 394 | e.reset_counters(); 395 | assert.equal(e.request_count, 500); 396 | }); 397 | 398 | it("sets requestsLastCheck = requestRate - pending", function () { 399 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 400 | e.pending = 500; 401 | e.request_rate = 600; 402 | e.reset_counters(); 403 | assert.equal(e.requests_last_check, 100); 404 | }); 405 | }); 406 | 407 | describe("ready()", function () { 408 | it("returns true when it is healthy and connected > pending with keepAlive on", 409 | function () { 410 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {keepAlive: true}); 411 | e.pending = 1; 412 | e.agent.sockets[e.name] = [1, 2]; 413 | assert(e.ready()); 414 | } 415 | ); 416 | 417 | it("returns false when it is healthy and connected = pending with keepAlive on", 418 | function () { 419 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {keepAlive: true}); 420 | e.pending = 1; 421 | e.agent.sockets[e.name] = [1]; 422 | assert(!e.ready()); 423 | } 424 | ); 425 | 426 | it("returns true when it is healthy and pending = 0 with keepAlive off", 427 | function () { 428 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 429 | e.pending = 0; 430 | assert(e.ready()); 431 | } 432 | ); 433 | 434 | it("returns false when it is healthy and pending > 0 with keepAlive off", 435 | function () { 436 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 437 | e.pending = 1; 438 | assert(!e.ready()); 439 | } 440 | ); 441 | }); 442 | 443 | describe("set_healthy()", function () { 444 | 445 | it("calls pinger.start if transitioning from healthy to unhealthy", function (done) { 446 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {ping: "/ping"}); 447 | var count = 0; 448 | e.pinger.start = function () { 449 | if (count === 0) { 450 | done(); 451 | } 452 | count++; 453 | }; 454 | e.set_healthy(false); 455 | }); 456 | 457 | it("emits 'health' once when changing state from healthy to unhealthy", function (done) { 458 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {ping: "/ping"}); 459 | e.emit = function (name) { 460 | assert.equal(name, "health"); 461 | done(); 462 | }; 463 | e.set_healthy(false); 464 | }); 465 | 466 | it("emits 'health' when changing state from unhealthy to healthy", function (done) { 467 | var e = new PoolEndpoint(http, "127.0.0.1", 6969, {ping: "/ping"}); 468 | var count = 0; 469 | e.emit = function (name) { 470 | assert.equal(name, "health"); 471 | if (count === 0) { 472 | done(); 473 | } 474 | count++; 475 | }; 476 | e.healthy = false; 477 | e.set_healthy(true); 478 | }); 479 | }); 480 | 481 | describe("close()", function () { 482 | it("aborts pending requests", function (done) { 483 | var failed; 484 | var s = http.createServer(function (req, res) { 485 | process.nextTick(function () { 486 | assert(failed); 487 | s.close(); 488 | done(); 489 | }); 490 | }).on("listening", function () { 491 | var e = new PoolEndpoint(http, "127.0.0.1", 6969); 492 | e.request({path: "/foo", method: "PUT", data: new Buffer("ƒoo")}, function (err, res) { 493 | assert(err); 494 | assert(!res); 495 | failed = true; 496 | }); 497 | e.close(); 498 | }); 499 | s.listen(6969); 500 | }); 501 | 502 | it("prevents pinger from requesting /", function (done) { 503 | var port = 9999; 504 | var e = new PoolEndpoint(http, "127.0.0.1", port, {ping: "/health"}); 505 | var s = http.createServer(function(req, res) { 506 | if (req.url === "/health") { 507 | e.close(); // Resets pinger.ping_path 508 | e.pinger.out_req.abort(); // Causes pinger request error 509 | e.pinger.out_req = null; // Nullifies pinger request to assert later 510 | s.close(function() { 511 | assert.equal(e.pinger.out_req, null, "out_req is not reset by subsequent request to /"); 512 | done(); 513 | }); 514 | } 515 | }); 516 | s.on("listening", function () { 517 | e.set_healthy(false); // Start pinger 518 | e.pinger.attempts = -1; // Hack to induce immediate pinger request 519 | }); 520 | s.listen(port); 521 | }); 522 | }); 523 | }); 524 | -------------------------------------------------------------------------------- /test/integration_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var assert = require("assert"); 4 | var http = require("http"); 5 | var Pool = require("../lb_pool")({}); 6 | 7 | describe("Pool request()", function () { 8 | it("passes options all the way to the endpoint request", function (done) { 9 | var pool = new Pool(http, ["127.0.0.1:6969"]); 10 | var s = http.createServer(function (req, res) { 11 | res.end("foo"); 12 | s.close(); 13 | }); 14 | s.on("listening", function () { 15 | pool.request({ 16 | path: "/foo", 17 | method: "GET", 18 | ca: "bar.ca" 19 | }, null, function () { 20 | done(); 21 | }); 22 | var req = pool.get_node().requests[0]; 23 | assert.equal(req.options.ca, "bar.ca"); 24 | }); 25 | s.listen(6969); 26 | }); 27 | 28 | it("retries failed requests on another node", function (done) { 29 | var req_count = 0; 30 | var listen_count = 0; 31 | var ports = [6960, 6961, 6962, 6963, 6964, 6965]; 32 | var servers = []; 33 | var endpoint_list = ports.map(function (port) { return "127.0.0.1:" + port; }); 34 | var pool = new Pool(http, endpoint_list, { ping: "/ping" }); 35 | var failed_port; 36 | function next() { 37 | pool.get({ 38 | path: "/foo", 39 | retry_delay: 0 40 | }, null, function (err, res, body) { 41 | assert.strictEqual(body, "OK"); 42 | servers.forEach(function (server) { server.close(); }); 43 | done(); 44 | }); 45 | } 46 | function on_request(port, req, res) { 47 | if (req.url === "/ping") { 48 | assert.strictEqual(port, failed_port); 49 | return req.socket.destroy(); 50 | } 51 | req_count++; 52 | if (req_count < 2) { 53 | failed_port = port; 54 | req.socket.destroy(); 55 | } else { 56 | assert.notStrictEqual(port, failed_port); 57 | res.end("OK"); 58 | } 59 | } 60 | function on_listening() { 61 | listen_count++; 62 | if (listen_count === ports.length) { 63 | next(); 64 | } 65 | } 66 | ports.forEach(function (port) { 67 | var server = http.createServer(on_request.bind(this, port)); 68 | server.listen(port); 69 | server.on("listening", on_listening); 70 | servers.push(server); 71 | }); 72 | }); 73 | 74 | it("allows specified requests to skip the max_pending check", function (done) { 75 | var port = 6969; 76 | var pool = new Pool(http, ["127.0.0.1:" + port], { ping: "/ping", max_pending: 1 }); 77 | var server; 78 | 79 | function on_listening() { 80 | var completed = 0; 81 | [1, 2, 3, 4].forEach(function (num) { 82 | pool.get({ 83 | path: "/foo/" + num, 84 | override_pending: true 85 | }, null, function (err, res, body) { 86 | assert.ifError(err); 87 | assert.strictEqual(body, "OK " + num); 88 | completed++; 89 | if (completed === 4) { 90 | server.close(); 91 | done(); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | function on_request(req, res) { 98 | var num = require("url").parse(req.url).pathname.split("/")[2]; 99 | res.end("OK " + num); 100 | } 101 | 102 | server = http.createServer(on_request); 103 | server.listen(port); 104 | server.on("listening", on_listening); 105 | }); 106 | 107 | it("reuses open sockets when making requests", function (done) { 108 | var ports = [6960, 6961, 6962, 6963, 6964]; 109 | 110 | var endpoint_list = ports.map(function hostPort(p) { 111 | return "127.0.0.1:" + p; 112 | }); 113 | 114 | var pool = new Pool(http, endpoint_list, { 115 | ping: "/ping", 116 | keep_alive: true, 117 | max_pending: 300, 118 | max_sockets: 2 119 | }); 120 | var servers = []; 121 | var listen_count = 0; 122 | 123 | function send_requests() { 124 | var completed = 0; 125 | var total = 10; 126 | var seenRemotes = []; 127 | 128 | send_a_request(); 129 | 130 | function send_a_request() { 131 | var req = pool.get({ 132 | path: "/foo/" + completed 133 | }, null, function (err, res, body) { 134 | var addr = res.socket.address(); 135 | 136 | if (seenRemotes.indexOf(addr.port) === -1) { 137 | seenRemotes.push(addr.port); 138 | } 139 | 140 | assert.ifError(err); 141 | assert.strictEqual(body, "OK " + completed); 142 | 143 | completed++; 144 | if (completed === total) { 145 | finish(); 146 | } else { 147 | send_a_request(); 148 | } 149 | }); 150 | 151 | var endpoint = req.endpoint; 152 | 153 | if (completed === 0) { 154 | assert.equal(endpoint.ready(), false); 155 | assert.equal(endpoint.stats().socket_count, 1); 156 | } else { 157 | assert.equal(endpoint.ready(), true); 158 | assert.equal(endpoint.stats().socket_count, 2); 159 | } 160 | } 161 | 162 | function finish() { 163 | assert.strictEqual(seenRemotes.length, 2); 164 | 165 | servers.forEach(function closeIt(s) { 166 | s.close(); 167 | }) 168 | done(); 169 | } 170 | } 171 | 172 | function on_request(req, res) { 173 | var num = require("url").parse(req.url).pathname.split("/")[2]; 174 | res.end("OK " + num); 175 | } 176 | 177 | function on_listening() { 178 | listen_count++; 179 | if (listen_count === ports.length) { 180 | send_requests(); 181 | } 182 | } 183 | 184 | ports.forEach(function (port) { 185 | var server = http.createServer(on_request); 186 | server.listen(port); 187 | server.on("listening", on_listening); 188 | servers.push(server); 189 | }); 190 | }); 191 | 192 | 193 | it("uses a specific endpoint if options.endpoint is set, even on retries", function (done) { 194 | var req_count = 0; 195 | var listen_count = 0; 196 | var ports = [6960, 6961, 6962, 6963, 6964, 6965]; 197 | var servers = []; 198 | var endpoint_list = ports.map(function (port) { return "127.0.0.1:" + port; }); 199 | var pool = new Pool(http, endpoint_list, { ping: "/ping" }); 200 | var failed_port; 201 | function next() { 202 | pool.get({ 203 | path: "/foo", 204 | endpoint: "127.0.0.1:6963", 205 | retry_delay: 100, 206 | max_attempts: 5, 207 | max_hangups: 4 208 | }, null, function (err, res, body) { 209 | assert.strictEqual(body, "OK"); 210 | servers.forEach(function (server) { server.close(); }); 211 | done(); 212 | }); 213 | } 214 | function on_request(port, req, res) { 215 | if (req.url === "/ping") { 216 | return res.end("pong"); 217 | } 218 | req_count++; 219 | if (req_count < 4) { 220 | failed_port = port; 221 | req.socket.destroy(); 222 | } else { 223 | assert.strictEqual(port, failed_port); 224 | res.end("OK"); 225 | } 226 | } 227 | function on_listening() { 228 | listen_count++; 229 | if (listen_count === ports.length) { 230 | next(); 231 | } 232 | } 233 | ports.forEach(function (port) { 234 | var server = http.createServer(on_request.bind(this, port)); 235 | server.listen(port); 236 | server.on("listening", on_listening); 237 | servers.push(server); 238 | }); 239 | }); 240 | 241 | it("fails if options.endpoint doesn't match anything", function (done) { 242 | var req_count = 0; 243 | var listen_count = 0; 244 | var ports = [6960, 6961, 6962, 6963, 6964, 6965]; 245 | var servers = []; 246 | var endpoint_list = ports.map(function (port) { return "127.0.0.1:" + port; }); 247 | var pool = new Pool(http, endpoint_list, { ping: "/ping" }); 248 | var failed_port; 249 | function next() { 250 | pool.get({ 251 | path: "/foo", 252 | endpoint: "127.0.0.1:9999", 253 | retry_delay: 100, 254 | max_attempts: 5, 255 | max_hangups: 4 256 | }, null, function (err) { 257 | assert(err); 258 | servers.forEach(function (server) { server.close(); }); 259 | done(); 260 | }); 261 | } 262 | function on_request(port, req, res) { 263 | if (req.url === "/ping") { 264 | assert.strictEqual(port, failed_port); 265 | return res.end("pong"); 266 | } 267 | req_count++; 268 | if (req_count < 4) { 269 | failed_port = port; 270 | req.socket.destroy(); 271 | } else { 272 | assert.strictEqual(port, failed_port); 273 | res.end("OK"); 274 | } 275 | } 276 | function on_listening() { 277 | listen_count++; 278 | if (listen_count === ports.length) { 279 | next(); 280 | } 281 | } 282 | ports.forEach(function (port) { 283 | var server = http.createServer(on_request.bind(this, port)); 284 | server.listen(port); 285 | server.on("listening", on_listening); 286 | servers.push(server); 287 | }); 288 | pool.on("retrying", function (err) { 289 | console.log("retrying in " + err.delay + "ms"); 290 | }); 291 | }); 292 | 293 | it("detects revived nodes with pinger", function (done) { 294 | var retry_count = 0; 295 | var listen_count = 0; 296 | var ports = [6960, 6961, 6962, 6963, 6964, 6965]; 297 | var servers = []; 298 | var endpoint_list = ports.map(function (port) { return "127.0.0.1:" + port; }); 299 | var pool = new Pool(http, endpoint_list, { ping: "/ping" }); 300 | function next() { 301 | pool.get({ 302 | path: "/foo", 303 | retry_delay: 100, 304 | max_aborts: 4 305 | }, null, function (err, res, body) { 306 | assert.strictEqual(body, "OK"); 307 | servers.forEach(function (server) { server.close(); }); 308 | done(); 309 | }); 310 | } 311 | function on_request(port, req, res) { 312 | if (req.url === "/ping") { 313 | return res.end("pong"); 314 | } 315 | res.end("OK"); 316 | } 317 | function on_listening() { 318 | listen_count++; 319 | } 320 | pool.on("retrying", function () { 321 | retry_count++; 322 | if (retry_count === 1) { 323 | ports.forEach(function (port) { 324 | var server = http.createServer(on_request.bind(this, port)); 325 | server.listen(port); 326 | server.on("listening", on_listening); 327 | servers.push(server); 328 | }); 329 | } 330 | }); 331 | next(); 332 | }); 333 | 334 | it("tracks pending count properly", function (done) { 335 | var req_count = 0; 336 | var listen_count = 0; 337 | 338 | var ports = [6960, 6961, 6962, 6963, 6964, 6965]; 339 | var servers = []; 340 | var endpoint_list = ports.map(function (port) { return "127.0.0.1:" + port; }); 341 | var pool = new Pool(http, endpoint_list, { ping: "/ping" }); 342 | 343 | function countPending() { 344 | var total = 0; 345 | 346 | for (var i = 0; i < pool.endpoints.length; i++) { 347 | total += pool.endpoints[i].pending; 348 | } 349 | 350 | return total; 351 | } 352 | 353 | function start() { 354 | var counter = 0; 355 | var total = 3; 356 | var responses = 3; 357 | 358 | for (var i = 0; i < total; i++) { 359 | pool.get({ 360 | path: "/foo", 361 | retry_delay: 0 362 | }, null, onResponse); 363 | console.log('pend', countPending()); 364 | assert.equal(countPending(), i + 1); 365 | } 366 | 367 | function onResponse(err, res, body) { 368 | assert.strictEqual(body, "OK"); 369 | 370 | responses--; 371 | console.log('pend', countPending()); 372 | assert.equal(countPending(), responses); 373 | counter++; 374 | if (counter === total) { 375 | onComplete(); 376 | } 377 | } 378 | } 379 | 380 | function onComplete() { 381 | servers.forEach(function (server) { 382 | server.close(); 383 | }); 384 | done(); 385 | } 386 | 387 | ports.forEach(function (port) { 388 | var server = http.createServer(on_request); 389 | server.listen(port); 390 | server.on("listening", on_listening); 391 | servers.push(server); 392 | 393 | function on_request(req, res) { 394 | req_count++; 395 | res.end("OK"); 396 | } 397 | 398 | function on_listening() { 399 | listen_count++; 400 | if (listen_count === ports.length) { 401 | start(); 402 | } 403 | } 404 | }); 405 | }); 406 | }); 407 | -------------------------------------------------------------------------------- /test/keep_alive_agent_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | /*global afterEach */ 4 | 5 | var assert = require("assert"), 6 | http = require("http"), 7 | https = require("https"), 8 | KeepAliveAgent = require("../keep_alive_agent")(); 9 | 10 | var server_config = { 11 | hostname: "localhost", 12 | port: 8000 13 | }; 14 | var socket_name = server_config.hostname + ":" + server_config.port; 15 | 16 | function make_test_request(agent, callback) { 17 | http.get({ 18 | hostname: server_config.hostname, 19 | port: server_config.port, 20 | path: "/", 21 | agent: agent 22 | }, callback); 23 | } 24 | 25 | describe("KeepAliveAgent", function () { 26 | var server; 27 | 28 | beforeEach(function (done) { 29 | var count = 0; 30 | server = http.createServer(function (request, response) { 31 | count++; 32 | response.end("pong " + count); 33 | }); 34 | server.on("listening", done); 35 | server.listen(server_config.port); 36 | }); 37 | 38 | afterEach(function () { 39 | server.close(); 40 | server = null; 41 | }); 42 | 43 | it("constructs an agent with the passed-in options", function () { 44 | var agent = new KeepAliveAgent.HTTP({ maxSockets: 3 }); 45 | 46 | assert.strictEqual(agent.maxSockets, 3, "max sockets option not passed through"); 47 | assert.strictEqual(typeof agent.idle_sockets, "object"); 48 | }); 49 | 50 | it("provides a socket to a request", function (done) { 51 | var agent = new KeepAliveAgent.HTTP(); 52 | http.get({ 53 | hostname: server_config.hostname, 54 | port: server_config.port, 55 | path: "/", 56 | agent: agent 57 | }, function () { 58 | // if we get here at all, it worked 59 | done(); 60 | }); 61 | }); 62 | 63 | it("re-uses sockets on repeated requests to the same host:port", function (done) { 64 | var agent = new KeepAliveAgent.HTTP(); 65 | var get_options = { 66 | hostname: server_config.hostname, 67 | port: server_config.port, 68 | path: "/", 69 | agent: agent 70 | }; 71 | 72 | var requests_todo = 10; 73 | var interval_id; 74 | 75 | var request_one = function () { 76 | get_options.path = "/" + requests_todo; 77 | http.get(get_options, function (res) { 78 | res.on("data", function on_data(chunk) {}); 79 | res.on("end", function on_end() { 80 | // HTTP cleanup needs to happen to trigger Agent socket behavior 81 | process.nextTick(function () { 82 | if (--requests_todo === 0) { 83 | assert.strictEqual(Array.isArray(agent.idle_sockets[socket_name]), true); 84 | assert.strictEqual(agent.idle_sockets[socket_name].length, 1); 85 | var socket = agent.idle_sockets[socket_name][0]; 86 | assert.strictEqual(socket.request_count, 10); 87 | done(); 88 | } else { 89 | request_one(); 90 | } 91 | }); 92 | }); 93 | }); 94 | }; 95 | 96 | request_one(); 97 | }); 98 | 99 | it("does not return destroyed sockets to the idle pool", function (done) { 100 | var agent = new KeepAliveAgent.HTTP(); 101 | make_test_request(agent, function (response) { 102 | process.nextTick(function () { 103 | response.connection.destroy(); 104 | process.nextTick(function () { 105 | assert.strictEqual(Object.keys(agent.idle_sockets).length, 0); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | it("does not attempt to use destroyed sockets from the idle list", function () { 113 | var agent = new KeepAliveAgent.HTTP(); 114 | 115 | agent.idle_sockets[socket_name] = []; 116 | agent.idle_sockets[socket_name].push({ destroyed: true }); 117 | agent.idle_sockets[socket_name].push({ destroyed: true }); 118 | agent.idle_sockets[socket_name].push({ destroyed: true }); 119 | agent.idle_sockets[socket_name].push({ destroyed: true }); 120 | 121 | var socket = agent.next_idle_socket(socket_name); 122 | assert.strictEqual(socket, null); 123 | assert.strictEqual(agent.idle_sockets[socket_name].length, 0); 124 | }); 125 | 126 | it("reuses a good socket until it is destroyed", function (done) { 127 | var agent = new KeepAliveAgent.HTTP(); 128 | 129 | make_test_request(agent, function (res) { 130 | res.on("data", function (chunk) {}); 131 | res.on("end", function () { 132 | process.nextTick(function () { 133 | assert.strictEqual(Array.isArray(agent.idle_sockets[socket_name]), true, "expected idle sockets list for " + socket_name + " to be an array"); 134 | assert.strictEqual(agent.idle_sockets[socket_name].length, 1, "expected idle sockets list to contain exactly 1 item"); 135 | var socket = agent.idle_sockets[socket_name][0]; 136 | assert.strictEqual(socket.request_count, 1, "expected socket request count to be 1"); 137 | 138 | socket.destroy(); 139 | 140 | process.nextTick(function () { 141 | make_test_request(agent, function (res) { 142 | res.on("data", function (chunk) {}); 143 | res.on("end", function () { 144 | process.nextTick(function () { 145 | assert.strictEqual(Array.isArray(agent.idle_sockets[socket_name]), true, "expected idle sockets list for " + socket_name + " to be an array"); 146 | assert.strictEqual(agent.idle_sockets[socket_name].length, 1, "expected idle sockets list to contain exactly 1 item"); 147 | var socket = agent.idle_sockets[socket_name][0]; 148 | assert.strictEqual(socket.request_count, 1, "expected socket request count to be 1"); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | it("closes the socket after max_reqs_per_socket requests", function (done) { 160 | var agent = new KeepAliveAgent.HTTP({max_reqs_per_socket: 2}); 161 | 162 | make_test_request(agent, function (response) { 163 | response.on("data", function (chunk) {}); 164 | response.on("end", function () { 165 | process.nextTick(function () { 166 | var socket = agent.idle_sockets[socket_name][0]; 167 | assert.strictEqual(socket.request_count, 1, "socket.request_count should be 1"); 168 | make_test_request(agent, function (response) { 169 | response.on("data", function (chunk) {}); 170 | response.on("end", function () { 171 | process.nextTick(function () { 172 | assert.strictEqual(agent.idle_sockets[socket_name].length, 0, "agent should have no idle sockets"); 173 | make_test_request(agent, function (response) { 174 | response.on("data", function (chunk) {}); 175 | response.on("end", function () { 176 | process.nextTick(function () { 177 | assert.strictEqual(agent.idle_sockets[socket_name][0].request_count, 1); 178 | assert(socket.destroyed); 179 | done(); 180 | }); 181 | }); 182 | }); 183 | }); 184 | }); 185 | }); 186 | }); 187 | }); 188 | }); 189 | }); 190 | }); 191 | 192 | describe("KeepAliveAgent.Secure", function () { 193 | var versions = process.version.split("."); 194 | if (versions[0] > 'v0') { 195 | it.skip("not supported in node4"); 196 | return; 197 | } 198 | 199 | it("can construct a secure keep-alive agent", function () { 200 | var secure_agent = new KeepAliveAgent.HTTPS({}); 201 | assert(secure_agent.defaultPort === 443); 202 | }); 203 | 204 | it("basically works", function (done) { 205 | https.get({ 206 | hostname: "one.voxer.com", 207 | port: 443, 208 | path: "/ping", 209 | agent: new KeepAliveAgent.HTTPS(), 210 | }, function () { 211 | done(); 212 | }); 213 | }); 214 | 215 | it("reuses sockets for secure connections", function (done) { 216 | var agent = new KeepAliveAgent.HTTPS(); 217 | var get_options = { 218 | hostname: "one.voxer.com", 219 | port: 443, 220 | path: "/ping", 221 | agent: agent, 222 | }; 223 | var socket_name = "one.voxer.com:443"; 224 | 225 | https.get(get_options, function (res) { 226 | res.on("data", function (chunk) {}); 227 | res.on("error", function (e) {}); 228 | res.on("end", function () { 229 | process.nextTick(function () { 230 | assert.strictEqual(Array.isArray(agent.idle_sockets[socket_name]), true, "expected idle sockets list for " + socket_name + " to be an array"); 231 | assert.strictEqual(agent.idle_sockets[socket_name].length, 1, "expected idle sockets list to contain exactly 1 item"); 232 | var socket = agent.idle_sockets[socket_name][0]; 233 | assert.strictEqual(socket.request_count, 1, "expected socket request count to be 1"); 234 | 235 | socket.destroy(); 236 | process.nextTick(function () { 237 | https.get(get_options, function (res) { 238 | res.on("data", function (chunk) {}); 239 | res.on("end", function () { 240 | process.nextTick(function () { 241 | assert.strictEqual(agent.idle_sockets[socket_name].length, 1, "expected idle sockets list to contain exactly 1 item"); 242 | done(); 243 | }); 244 | }); 245 | }); 246 | }); 247 | }); 248 | }); 249 | }); 250 | }); 251 | 252 | it("does not attempt to use destroyed sockets from the idle list", function () { 253 | var agent = new KeepAliveAgent.HTTPS(); 254 | 255 | agent.idle_sockets[socket_name] = []; 256 | agent.idle_sockets[socket_name].push({ pair: { ssl: null } }); 257 | agent.idle_sockets[socket_name].push({ pair: { ssl: null } }); 258 | agent.idle_sockets[socket_name].push({ pair: { ssl: null } }); 259 | agent.idle_sockets[socket_name].push({ pair: { ssl: null } }); 260 | agent.idle_sockets[socket_name].push({ pair: { ssl: null } }); 261 | 262 | var socket = agent.next_idle_socket(socket_name); 263 | assert.equal(socket, null); 264 | assert.equal(agent.idle_sockets[socket_name].length, 0); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /test/pool_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var assert = require("assert"); 4 | 5 | var noop = function () {}; 6 | 7 | function FakeEndpointRequest(endpoint, options, callback) { 8 | this.endpoint = endpoint; 9 | this.options = options; 10 | this.done = this.callback = callback; 11 | 12 | this.id = endpoint.request_count++; 13 | } 14 | 15 | FakeEndpointRequest.prototype.start = function () {}; 16 | 17 | var success_body = "success body"; 18 | 19 | function start_with_success() { 20 | this.endpoint.request_succeeded(this, { statusCode: 200 }, success_body); 21 | } 22 | function start_with_success_reuse() { 23 | this.endpoint.request_succeeded(this, { statusCode: 200, socket: { request_count: 2 } }, success_body); 24 | } 25 | function start_with_fail() { 26 | this.endpoint.request_failed({ 27 | message: "failed request", 28 | reason: "unreasonably failed" 29 | }, this); 30 | } 31 | 32 | var http = { 33 | request: noop, 34 | Agent: noop 35 | }; 36 | var GO = {}; 37 | 38 | var Pool; 39 | 40 | GO.PoolEndpoint = require("../pool_endpoint")(GO); 41 | GO.PoolRequestSet = require("../pool_request_set")(GO); 42 | GO.PoolEndpointRequest = FakeEndpointRequest; 43 | GO.KeepAliveAgent = require("../keep_alive_agent")(GO); 44 | GO.PoolPinger = require("../pool_pinger")(GO); 45 | Pool = require("../pool")(GO); 46 | 47 | describe("Pool", function () { 48 | it("throws an Error if constructed with no endpoints", function () { 49 | assert.throws(function () { 50 | return new Pool(); 51 | }); 52 | }); 53 | 54 | it("throws an Error when the node list is invalid", function () { 55 | assert.throws(function () { 56 | return new Pool(http, ["foo_bar"]); 57 | }); 58 | }); 59 | 60 | it("throws an Error when http is invalid", function () { 61 | assert.throws(function () { 62 | return new Pool({}, ["127.0.0.1:8080"]); 63 | }); 64 | }); 65 | 66 | it("sets this.length to this.endpoints.length", function () { 67 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 68 | assert.equal(p.length, 3); 69 | }); 70 | 71 | it("sets limits endpoints to max_pool_size option", function () { 72 | var endpoints = [ 73 | "127.0.0.1:8080", 74 | "127.0.0.1:8081", 75 | "127.0.0.1:8082", 76 | "127.0.0.1:8083", 77 | "127.0.0.1:8084", 78 | "127.0.0.1:8085", 79 | "127.0.0.1:8086" 80 | ]; 81 | var p = new Pool(http, endpoints, {max_pool_size: 2}); 82 | assert.equal(p.length, 2); 83 | }); 84 | 85 | it("selecting endpoints from max_pool_size should be random", function () { 86 | var endpoints = [ 87 | "127.0.0.1:8080", 88 | "127.0.0.1:8081", 89 | "127.0.0.1:8082", 90 | "127.0.0.1:8083", 91 | "127.0.0.1:8084", 92 | "127.0.0.1:8085", 93 | "127.0.0.1:8086" 94 | ]; 95 | 96 | var pools = [ 97 | new Pool(http, endpoints, {max_pool_size: 2}), 98 | new Pool(http, endpoints, {max_pool_size: 2}), 99 | new Pool(http, endpoints, {max_pool_size: 2}), 100 | new Pool(http, endpoints, {max_pool_size: 2}), 101 | new Pool(http, endpoints, {max_pool_size: 2}), 102 | new Pool(http, endpoints, {max_pool_size: 2}), 103 | new Pool(http, endpoints, {max_pool_size: 2}) 104 | ]; 105 | 106 | var seenEndpoints = {}; 107 | for (var i = 0; i < pools.length; i++) { 108 | var p = pools[i]; 109 | var endpointsByName = Object.keys(p.endpoints_by_name); 110 | for (var j = 0; j < endpointsByName.length; j++) { 111 | var hostPort = endpointsByName[j]; 112 | if (!seenEndpoints[hostPort]) { 113 | seenEndpoints[hostPort] = 1; 114 | } else { 115 | seenEndpoints[hostPort]++; 116 | } 117 | } 118 | } 119 | 120 | // Should see at least 75% of endpoints picked. 121 | var numberOfEndpoints = Object.keys(seenEndpoints).length; 122 | assert.ok(numberOfEndpoints >= 5) 123 | }); 124 | 125 | it("throws an Error when options.max_pool_size is invalid", function () { 126 | assert.throws(function () { 127 | return new Pool({}, ["127.0.0.1:8080"], {max_pool_size: -1}); 128 | }); 129 | }); 130 | 131 | describe("add/remove endpoints", function () { 132 | var endpoints = [ 133 | "127.0.0.1:8080", 134 | "127.0.0.1:8081", 135 | "127.0.0.1:8082" 136 | ]; 137 | 138 | it("adds endpoint", function () { 139 | var ep = endpoints.slice(0); 140 | var p = new Pool(http, ep); 141 | p.add_endpoint("127.0.0.1:8086") 142 | assert.equal(p.length, 4); 143 | }); 144 | 145 | it("filters invalid port", function () { 146 | var ep = endpoints.slice(0); 147 | var p = new Pool(http, ep); 148 | p.add_endpoint("127.0.0.1:999999"); 149 | assert.equal(p.length, 3); 150 | }); 151 | 152 | it("filters invalid hostport no sep", function () { 153 | var ep = endpoints.slice(0); 154 | var p = new Pool(http, ep); 155 | p.add_endpoint("localhost999999"); 156 | assert.equal(p.length, 3); 157 | }); 158 | 159 | 160 | it("adds endpoints according to max_pool_size", function () { 161 | var ep = endpoints.slice(0); 162 | var p = new Pool(http, ep, {max_pool_size: 2}); 163 | assert.equal(p.length, 2); 164 | 165 | for (var i = 0; i < 20; i++) { 166 | var port = 8090 + i; 167 | p.add_endpoint("127.0.0.1:" + port); 168 | } 169 | assert.equal(p.length, 2); 170 | }); 171 | 172 | it("adds endpoints with larger max_pool_size", function () { 173 | var ep = endpoints.slice(0); 174 | var p = new Pool(http, ep, {max_pool_size: 20}); 175 | assert.equal(p.length, ep.length); 176 | 177 | for (var i = 0; i < 20; i++) { 178 | var port = 8090 + i; 179 | p.add_endpoint("127.0.0.1:" + port); 180 | var expSize = Math.min(20, i + 1 + ep.length); 181 | assert.equal(p.length, expSize); 182 | } 183 | }); 184 | 185 | it("removes endpoints", function () { 186 | var ep = endpoints.slice(0); 187 | var p = new Pool(http, ep); 188 | assert.equal(p.length, 3); 189 | p.remove_endpoint("127.0.0.1:8080") 190 | assert.equal(p.length, 2); 191 | }) 192 | 193 | it("removing non-existant endpoint is no-op", function () { 194 | var ep = endpoints.slice(0); 195 | var p = new Pool(http, ep); 196 | assert.equal(p.length, 3); 197 | p.remove_endpoint("127.0.0.1:8089") 198 | assert.equal(p.length, 3); 199 | }) 200 | 201 | it("removes endpoints with max_pool_size set", function () { 202 | var ep = endpoints.slice(0); 203 | ep.push("127.0.0.1:8083") 204 | ep.push("127.0.0.1:8084") 205 | var p = new Pool(http, ep, {maxPoolSize: 3}); 206 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 207 | p.add_endpoint("127.0.0.1:8085"); 208 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 209 | assert.equal(p.all_hostports.length, 6); 210 | assert.equal(p.length, 3); 211 | p.remove_endpoint("127.0.0.1:8085") 212 | assert.equal(p.all_hostports.length, 5); 213 | assert.equal(p.length, 3); 214 | p.remove_endpoint("127.0.0.1:8084") 215 | assert.equal(p.all_hostports.length, 4); 216 | assert.equal(p.length, 3); 217 | p.remove_endpoint("127.0.0.1:8083") 218 | assert.equal(p.all_hostports.length, 3); 219 | assert.equal(p.length, 3); 220 | p.remove_endpoint("127.0.0.1:8082") 221 | assert.equal(p.all_hostports.length, 2); 222 | assert.equal(p.length, 2); 223 | p.remove_endpoint("127.0.0.1:8081") 224 | assert.equal(p.all_hostports.length, 1); 225 | assert.equal(p.length, 1); 226 | }) 227 | 228 | it("removes endpoints works with max_size > all_hostports", function () { 229 | var ep = endpoints.slice(0); 230 | ep.push("127.0.0.1:8083") 231 | ep.push("127.0.0.1:8084") 232 | var p = new Pool(http, ep, {maxPoolSize: 20}); 233 | assert.equal(Object.keys(p.endpoints_by_name).length, 5); 234 | p.add_endpoint("127.0.0.1:8085"); 235 | assert.equal(Object.keys(p.endpoints_by_name).length, 6); 236 | assert.equal(p.all_hostports.length, 6); 237 | assert.equal(p.length, 6); 238 | p.remove_endpoint("127.0.0.1:8085") 239 | assert.equal(p.all_hostports.length, 5); 240 | assert.equal(p.length, 5); 241 | p.remove_endpoint("127.0.0.1:8084") 242 | assert.equal(p.all_hostports.length, 4); 243 | assert.equal(p.length, 4); 244 | p.remove_endpoint("127.0.0.1:8083") 245 | assert.equal(p.all_hostports.length, 3); 246 | assert.equal(p.length, 3); 247 | p.remove_endpoint("127.0.0.1:8082") 248 | assert.equal(p.all_hostports.length, 2); 249 | assert.equal(p.length, 2); 250 | p.remove_endpoint("127.0.0.1:8081") 251 | assert.equal(p.all_hostports.length, 1); 252 | assert.equal(p.length, 1); 253 | }) 254 | }); 255 | 256 | describe("adjust_pool_size", function () { 257 | var endpoints = [ 258 | "127.0.0.1:8080", 259 | "127.0.0.1:8081", 260 | "127.0.0.1:8082", 261 | "127.0.0.1:8083", 262 | "127.0.0.1:8084" 263 | ]; 264 | 265 | it("adjust is no-op if max_pool_size not set", function () { 266 | var ep = endpoints.slice(0); 267 | var p = new Pool(http, ep); 268 | assert.equal(p.length, 5); 269 | assert.equal(p.all_hostports.length, 5); 270 | p.adjust_pool_size(); 271 | assert.equal(p.length, 5); 272 | assert.equal(p.all_hostports.length, 5); 273 | }); 274 | 275 | it("adjust removes pool endpoints if there are too many", function () { 276 | var ep = endpoints.slice(0); 277 | var p = new Pool(http, ep, {maxPoolSize: 3}); 278 | assert.equal(p.all_hostports.length, 5); 279 | assert.equal(p.length, 3); 280 | assert.equal(p.endpoints.length, 3); 281 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 282 | 283 | for (var i = 0; i < endpoints.length; i++) { 284 | p.add_pool_endpoint(endpoints[i]); 285 | } 286 | assert.equal(p.length, 5); 287 | assert.equal(p.endpoints.length, 5); 288 | assert.equal(Object.keys(p.endpoints_by_name).length, 5); 289 | 290 | p.adjust_pool_size(); 291 | 292 | assert.equal(p.length, 3); 293 | assert.equal(p.endpoints.length, 3); 294 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 295 | }); 296 | 297 | it("adjust adds pool endpoints if there are too few", function () { 298 | var ep = endpoints.slice(0); 299 | var p = new Pool(http, ep, {maxPoolSize: 3}); 300 | assert.equal(p.all_hostports.length, 5); 301 | assert.equal(p.length, 3); 302 | assert.equal(p.endpoints.length, 3); 303 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 304 | 305 | for (var i = 0; i < endpoints.length; i++) { 306 | p.remove_pool_endpoint(endpoints[i]); 307 | } 308 | assert.equal(p.length, 0); 309 | assert.equal(p.endpoints.length, 0); 310 | assert.equal(Object.keys(p.endpoints_by_name).length, 0); 311 | 312 | p.adjust_pool_size(); 313 | 314 | assert.equal(p.length, 3); 315 | assert.equal(p.endpoints.length, 3); 316 | assert.equal(Object.keys(p.endpoints_by_name).length, 3); 317 | }); 318 | }) 319 | 320 | 321 | describe("healthy_endpoints()", function () { 322 | it("filters out unhealthy endpoints from the result", function () { 323 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 324 | p.endpoints[0].healthy = false; 325 | assert.equal(true, p.healthy_endpoints().every(function (n) { 326 | return n.healthy; 327 | })); 328 | }); 329 | }); 330 | 331 | describe("get_endpoint()", function () { 332 | it("returns the 'overloaded' endpoint when total_pending > max_pending", function () { 333 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"], { max_pending: 30 }); 334 | p.endpoints.forEach(function (n) { n.pending = 10; }); 335 | assert.equal(p.get_endpoint().special_endpoint, "overloaded"); 336 | }); 337 | 338 | it("returns the 'unhealthy' endpoint when no endpoints are healthy", function () { 339 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 340 | p.endpoints.forEach(function (n) { n.healthy = false; }); 341 | assert.equal(p.get_endpoint().special_endpoint, "unhealthy"); 342 | }); 343 | 344 | it("returns a 'ready' endpoint when one is available", function () { 345 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 346 | var n = p.endpoints[0]; 347 | n.ready = function () { return true; }; 348 | n.test_flag = true; 349 | p.endpoints[1].ready = function () { return false; }; 350 | p.endpoints[2].ready = function () { return false; }; 351 | assert.equal(p.get_endpoint().test_flag, true); 352 | }); 353 | 354 | it("returns a healthy endpoint when at least one is 'ready'", function () { 355 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 356 | p.endpoints[0].healthy = false; 357 | p.endpoints[1].healthy = false; 358 | p.endpoints[2].healthy = true; 359 | assert(p.get_endpoint().healthy); 360 | }); 361 | }); 362 | 363 | describe("unhealthy_endpoint", function () { 364 | it("returns a 'unhealthy' error on request", function () { 365 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 366 | p.unhealthy_endpoint.request({}, function (err) { 367 | assert.equal(err.reason, "unhealthy"); 368 | }); 369 | }); 370 | 371 | it("is not healthy", function () { 372 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 373 | assert.equal(false, p.unhealthy_endpoint.healthy); 374 | }); 375 | }); 376 | 377 | describe("overloaded_endpoint", function () { 378 | it("returns a 'full' error on request", function () { 379 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 380 | p.overloaded_endpoint.request({}, function (err) { 381 | assert.equal(err.reason, "full"); 382 | }); 383 | }); 384 | 385 | it("is not healthy", function () { 386 | var p = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 387 | assert.equal(false, p.overloaded_endpoint.healthy); 388 | }); 389 | }); 390 | 391 | describe("request()", function () { 392 | it("calls callback with response on success", function (done) { 393 | FakeEndpointRequest.prototype.start = start_with_success; 394 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 395 | pool.request({}, null, function (err, res, body) { 396 | assert.equal(body, success_body); 397 | done(); 398 | }); 399 | }); 400 | 401 | it("calls callback with error on failure", function (done) { 402 | FakeEndpointRequest.prototype.start = start_with_fail; 403 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 404 | pool.request({}, null, function (err, res, body) { 405 | assert.strictEqual(err.message, "failed request"); 406 | assert.strictEqual(res, undefined); 407 | assert.strictEqual(body, undefined); 408 | done(); 409 | }); 410 | }); 411 | 412 | it("emits timing on success", function (done) { 413 | FakeEndpointRequest.prototype.start = start_with_success; 414 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 415 | pool.on("timing", function () { 416 | done(); 417 | }); 418 | pool.request({}, null, noop); 419 | }); 420 | 421 | it("emits timing on failure", function (done) { 422 | FakeEndpointRequest.prototype.start = start_with_fail; 423 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 424 | pool.on("timing", function () { 425 | done(); 426 | }); 427 | pool.request({}, null, noop); 428 | }); 429 | 430 | it("sets the reused field of options to true when the socket is reused", function (done) { 431 | FakeEndpointRequest.prototype.start = start_with_success_reuse; 432 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 433 | pool.on("timing", function (interval, options) { 434 | assert(options.reused); 435 | done(); 436 | }); 437 | pool.request({}, null, noop); 438 | }); 439 | 440 | it("sets the reused field of options to false when the socket isn't reused", function (done) { 441 | FakeEndpointRequest.prototype.start = start_with_success; 442 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 443 | pool.on("timing", function (interval, options) { 444 | assert.equal(options.reused, false); 445 | done(); 446 | }); 447 | pool.request({}, null, noop); 448 | }); 449 | 450 | it("allows the data parameter to be optional", function (done) { 451 | FakeEndpointRequest.prototype.start = start_with_success; 452 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 453 | pool.request({}, function (err, res, body) { 454 | assert.equal(res.statusCode, 200); 455 | assert.equal(body, success_body); 456 | done(); 457 | }); 458 | }); 459 | 460 | it("allows the options parameter to be a path string", function (done) { 461 | FakeEndpointRequest.prototype.start = start_with_success; 462 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 463 | pool.on("timing", function (interval, options) { 464 | assert.equal(options.path, "/foo"); 465 | done(); 466 | }); 467 | pool.request("/foo", function (err, res, body) { 468 | assert.equal(res.statusCode, 200); 469 | assert.equal(body, success_body); 470 | }); 471 | }); 472 | 473 | it("defaults method to GET", function (done) { 474 | FakeEndpointRequest.prototype.start = start_with_success; 475 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 476 | pool.on("timing", function (interval, options) { 477 | assert.equal(options.method, "GET"); 478 | done(); 479 | }); 480 | pool.request("/foo", function (err, res, body) { 481 | assert.equal(res.statusCode, 200); 482 | assert.equal(body, success_body); 483 | }); 484 | }); 485 | 486 | it("causes pool to emit response event", function (done) { 487 | var doneCounter = 0; 488 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 489 | 490 | function doneOne() { 491 | if (++doneCounter === 2) { 492 | done(); 493 | } 494 | } 495 | 496 | function assertResponse(assertions) { 497 | pool.once("response", function (err, poolReq, res) { 498 | assertions(err, poolReq, res); 499 | assert.ok(poolReq); 500 | assert.equal(poolReq.options.path, "/foo"); 501 | doneOne(); 502 | }); 503 | } 504 | 505 | function requestFoo() { 506 | pool.request("/foo", function() {}); 507 | } 508 | 509 | // Without error 510 | FakeEndpointRequest.prototype.start = start_with_success; 511 | assertResponse(function(err, poolReq, res) { 512 | assert.ifError(err); 513 | assert.ok(res); 514 | }); 515 | requestFoo(); 516 | 517 | // With error 518 | FakeEndpointRequest.prototype.start = start_with_fail; 519 | assertResponse(function(err, poolReq, res) { 520 | assert.ok(err); 521 | assert.equal(res, undefined); 522 | }); 523 | requestFoo(); 524 | }); 525 | }); 526 | 527 | describe("get()", function () { 528 | it("is an alias to request()", function (done) { 529 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 530 | assert.equal(pool.get, pool.request); 531 | done(); 532 | }); 533 | }); 534 | 535 | describe("put()", function () { 536 | it("sets the options.method to PUT", function (done) { 537 | FakeEndpointRequest.prototype.start = start_with_success; 538 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 539 | pool.on("timing", function (interval, options) { 540 | assert.equal(options.method, "PUT"); 541 | }); 542 | pool.put("/foo", "bar", function (err, res, body) { 543 | assert.strictEqual(err, null); 544 | assert.strictEqual(res.statusCode, 200); 545 | assert.strictEqual(body, success_body); 546 | done(); 547 | }); 548 | }); 549 | }); 550 | 551 | describe("post()", function () { 552 | it("sets the options.method to POST", function (done) { 553 | FakeEndpointRequest.prototype.start = start_with_success; 554 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 555 | pool.on("timing", function (interval, options) { 556 | assert.equal(options.method, "POST"); 557 | }); 558 | pool.post("/foo", "bar", function (err, res, body) { 559 | assert.strictEqual(err, null); 560 | assert.strictEqual(res.statusCode, 200); 561 | assert.strictEqual(body, success_body); 562 | done(); 563 | }); 564 | }); 565 | }); 566 | 567 | describe("del()", function () { 568 | it("sets the options.method to del", function (done) { 569 | FakeEndpointRequest.prototype.start = start_with_success; 570 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]); 571 | pool.on("timing", function (interval, options) { 572 | assert.equal(options.method, "DELETE"); 573 | }); 574 | pool.del("/foo", function (err, res, body) { 575 | assert.strictEqual(err, null); 576 | assert.strictEqual(res.statusCode, 200); 577 | assert.strictEqual(body, success_body); 578 | done(); 579 | }); 580 | }); 581 | }); 582 | 583 | describe("remove_endpoint()", function () { 584 | it("fails future requests", function (done) { 585 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081"]); 586 | pool.remove_endpoint("127.0.0.1:8081"); 587 | pool.get({endpoint: "127.0.0.1:8081", path: "/"}, function (err, res) { 588 | assert(err); 589 | assert(!res); 590 | done(); 591 | }); 592 | }); 593 | }); 594 | 595 | describe("close()", function () { 596 | it("does not fail", function (done) { 597 | var pool = new Pool(http, ["127.0.0.1:8080", "127.0.0.1:8081"]); 598 | pool.close(); 599 | done(); 600 | }); 601 | }); 602 | }); 603 | -------------------------------------------------------------------------------- /test/requestset_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Voxer IP LLC. All rights reserved. 2 | 3 | var assert = require("assert"); 4 | 5 | var PoolRequestSet; 6 | 7 | var endpoint = { 8 | request: function () {} 9 | }; 10 | 11 | var unhealthy = { 12 | request: function (options, callback) { callback({ message: "no endpoints"}); } 13 | }; 14 | 15 | function succeeding_request(options, cb) { 16 | return cb(null, {}, "foo"); 17 | } 18 | 19 | function failing_request(options, cb) { 20 | return cb({ 21 | message: "crap", 22 | reason: "ihateyou" 23 | }); 24 | } 25 | 26 | function hangup_request(options, cb) { 27 | return cb({ 28 | message: "hang up", 29 | reason: "socket hang up" 30 | }); 31 | } 32 | 33 | function aborted_request(options, cb) { 34 | return cb({ 35 | message: "aborted", 36 | reason: "aborted" 37 | }); 38 | } 39 | 40 | function timeout_request(options, cb) { 41 | return cb({ 42 | message: "timed out", 43 | reason: "timed_out" 44 | }); 45 | } 46 | 47 | var pool = { 48 | options: { max_retries: 5 }, 49 | get_endpoint: function () { 50 | return endpoint; 51 | }, 52 | on_retry: function () {}, 53 | length: 3 54 | }; 55 | 56 | PoolRequestSet = require("../pool_request_set")({}); 57 | 58 | describe("PoolRequestSet", function () { 59 | it("defaults attempt count to at least 2", function () { 60 | var r = new PoolRequestSet({length: 1, options: { max_retries: 5 }}, {}, null); 61 | assert.equal(r.max_attempts, 2); 62 | }); 63 | 64 | it("defaults attempt count to at most max_retries + 1", function () { 65 | var r = new PoolRequestSet({length: 9, options: { max_retries: 4 }}, {}, null); 66 | assert.equal(r.max_attempts, 5); 67 | }); 68 | 69 | it("defaults attempt count to pool.length", function () { 70 | var r = new PoolRequestSet({length: 4, options: { max_retries: 5 }}, {}, null); 71 | assert.equal(r.max_attempts, 4); 72 | }); 73 | 74 | describe("do_request()", function () { 75 | it("calls the callback on success", function (done) { 76 | var r = new PoolRequestSet(pool, {}, function (err, res, body) { 77 | assert.equal(err, null); 78 | assert.equal(body, "foo"); 79 | done(); 80 | }); 81 | endpoint.request = succeeding_request; 82 | r.do_request(); 83 | }); 84 | 85 | it("calls the callback on error", function (done) { 86 | var r = new PoolRequestSet(pool, {}, function (err, res, body) { 87 | assert.strictEqual(err.message, "crap"); 88 | assert.strictEqual(res, undefined); 89 | assert.strictEqual(body, undefined); 90 | done(); 91 | }); 92 | endpoint.request = failing_request; 93 | r.do_request(); 94 | }); 95 | 96 | it("calls the callback with a 'no endpoints' error when there's no endpoints to service the request", function (done) { 97 | var p = { 98 | options: { max_retries: 5 }, 99 | get_endpoint: function () { return unhealthy; }, 100 | length: 0, 101 | on_retry: function () {} 102 | }; 103 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 104 | assert.strictEqual(err.message, "no endpoints"); 105 | assert.strictEqual(res, undefined); 106 | assert.strictEqual(body, undefined); 107 | done(); 108 | }); 109 | r.do_request(); 110 | }); 111 | 112 | it("retries hangups once", function (done) { 113 | var i = 0; 114 | var p = { 115 | options: { max_retries: 5 }, 116 | get_endpoint: function () { return this.endpoints[i++]; }, 117 | on_retry: function () {}, 118 | length: 2, 119 | endpoints: [{ request: hangup_request }, { request: succeeding_request }] 120 | }; 121 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 122 | assert.equal(err, null); 123 | assert.equal(body, "foo"); 124 | done(); 125 | }); 126 | r.do_request(); 127 | }); 128 | 129 | it("retries hangups once then fails", function (done) { 130 | var p = { 131 | i: 0, 132 | options: { max_retries: 5 }, 133 | get_endpoint: function () { return this.endpoints[this.i++]; }, 134 | on_retry: function () {}, 135 | length: 3, 136 | endpoints: [{ request: hangup_request }, { request: hangup_request }, { request: succeeding_request }] 137 | }; 138 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 139 | assert.strictEqual(err.reason, "socket hang up"); 140 | assert.strictEqual(res, undefined); 141 | assert.strictEqual(body, undefined); 142 | done(); 143 | }); 144 | r.do_request(); 145 | }); 146 | 147 | it("retries aborts once", function (done) { 148 | var p = { 149 | i: 0, 150 | options: { max_retries: 5 }, 151 | get_endpoint: function () { return this.endpoints[this.i++]; }, 152 | on_retry: function () {}, 153 | length: 2, 154 | endpoints: [{ request: aborted_request }, { request: succeeding_request }] 155 | }; 156 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 157 | assert.equal(err, null); 158 | assert.equal(body, "foo"); 159 | done(); 160 | }); 161 | r.do_request(); 162 | }); 163 | 164 | it("retries aborts once then fails", function (done) { 165 | var p = { 166 | i: 0, 167 | options: { max_retries: 5 }, 168 | get_endpoint: function () { return this.endpoints[this.i++]; }, 169 | on_retry: function () {}, 170 | length: 3, 171 | endpoints: [{ request: aborted_request }, { request: aborted_request }, { request: succeeding_request }] 172 | }; 173 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 174 | assert.strictEqual(err.reason, "aborted"); 175 | assert.strictEqual(res, undefined); 176 | assert.strictEqual(body, undefined); 177 | done(); 178 | }); 179 | r.do_request(); 180 | }); 181 | 182 | it("retries timeouts once", function (done) { 183 | var i = 0; 184 | var p = { 185 | options: { max_retries: 5 }, 186 | get_endpoint: function () { return this.endpoints[i++]; }, 187 | on_retry: function () {}, 188 | length: 2, 189 | endpoints: [{ request: timeout_request }, { request: succeeding_request }] 190 | }; 191 | var r = new PoolRequestSet(p, { max_timeouts: 2 }, function (err, res, body) { 192 | assert.equal(err, null); 193 | assert.equal(body, "foo"); 194 | done(); 195 | }); 196 | r.do_request(); 197 | }); 198 | 199 | it("no retries on timeouts by default", function (done) { 200 | var i = 0; 201 | var p = { 202 | options: { max_retries: 5 }, 203 | get_endpoint: function () { return this.endpoints[i++]; }, 204 | on_retry: function () {}, 205 | length: 2, 206 | endpoints: [{ request: timeout_request }, { request: succeeding_request }] 207 | }; 208 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 209 | assert.strictEqual(err.reason, "timed_out"); 210 | assert.strictEqual(res, undefined); 211 | assert.strictEqual(body, undefined); 212 | done(); 213 | }); 214 | r.do_request(); 215 | }); 216 | 217 | it("fail, fail, then abort will call back with 'aborted'", function (done) { 218 | var p = { 219 | i: 0, 220 | options: { max_retries: 5 }, 221 | get_endpoint: function () { return this.endpoints[this.i++]; }, 222 | on_retry: function () {}, 223 | length: 3, 224 | endpoints: [{ request: failing_request }, { request: failing_request }, { request: aborted_request }] 225 | }; 226 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 227 | assert.strictEqual(err.reason, "aborted"); 228 | assert.strictEqual(res, undefined); 229 | assert.strictEqual(body, undefined); 230 | done(); 231 | }); 232 | r.do_request(); 233 | }); 234 | 235 | it("retries up to the first success", function (done) { 236 | var p = { 237 | i: 0, 238 | options: { max_retries: 5 }, 239 | get_endpoint: function () { return this.endpoints[this.i++]; }, 240 | on_retry: function () {}, 241 | length: 4, 242 | endpoints: [{ request: failing_request }, { request: failing_request }, { request: succeeding_request }, { request: failing_request }] 243 | }; 244 | var r = new PoolRequestSet(p, {}, function (err, res, body) { 245 | assert.equal(err, null); 246 | assert.equal(body, "foo"); 247 | done(); 248 | }); 249 | r.do_request(); 250 | }); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | BASEDIR=$(dirname $0) 6 | FILES=$BASEDIR/*_test.js 7 | for f in $FILES 8 | do 9 | echo "====== Running $f..." 10 | ./node_modules/.bin/mocha $f 11 | sleep 3 12 | done 13 | --------------------------------------------------------------------------------