├── .busted ├── .editorconfig ├── .gitignore ├── .luacheckrc ├── .travis.yml ├── LICENSE ├── README.md ├── config.ld ├── docs ├── index.html ├── ldoc.css ├── modules │ ├── resty.dns.balancer.base.html │ ├── resty.dns.balancer.consistent_hashing.html │ ├── resty.dns.balancer.handle.html │ ├── resty.dns.balancer.least_connections.html │ ├── resty.dns.balancer.round_robin.html │ ├── resty.dns.client.html │ └── resty.dns.utils.html └── topics │ └── README.md.html ├── examples ├── client.lua └── utils.lua ├── extra ├── README.md └── clientlog.lua ├── lua-resty-dns-client-6.0.2-1.rockspec ├── rbusted ├── spec ├── balancer │ ├── base_spec.lua │ ├── consistent_hashing_spec.lua │ ├── generic_spec.lua │ ├── handle_spec.lua │ ├── least_connections_spec.lua │ └── round_robin_spec.lua ├── client_cache_spec.lua ├── client_spec.lua ├── resty-runner.lua ├── test_helpers.lua └── utils_spec.lua ├── src └── resty │ └── dns │ ├── balancer │ ├── base.lua │ ├── consistent_hashing.lua │ ├── handle.lua │ ├── least_connections.lua │ └── round_robin.lua │ ├── client.lua │ └── utils.lua └── t ├── 00-sanity.t ├── 01-phases.t └── 02-timer-usage.t /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | lua = "spec/resty-runner.lua", 4 | verbose = true, 5 | coverage = false, 6 | output = "gtest", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.lua] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | *.rock 3 | t/servroot/ 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = false 5 | 6 | 7 | globals = { 8 | --"_KONG", 9 | --"kong", 10 | --"ngx.IS_CLI", 11 | } 12 | 13 | 14 | not_globals = { 15 | "string.len", 16 | "table.getn", 17 | } 18 | 19 | 20 | ignore = { 21 | --"6.", -- ignore whitespace warnings 22 | } 23 | 24 | 25 | exclude_files = { 26 | --"spec/fixtures/invalid-module.lua", 27 | --"spec-old-api/fixtures/invalid-module.lua", 28 | } 29 | 30 | 31 | --files["kong/plugins/ldap-auth/*.lua"] = { 32 | -- read_globals = { 33 | -- "bit.mod", 34 | -- "string.pack", 35 | -- "string.unpack", 36 | -- }, 37 | --} 38 | 39 | 40 | files["spec/**/*.lua"] = { 41 | std = "ngx_lua+busted", 42 | } 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: c 4 | 5 | compiler: gcc 6 | 7 | notifications: 8 | email: false 9 | 10 | cache: 11 | directories: 12 | - download-cache 13 | - perl5 14 | 15 | env: 16 | global: 17 | - JOBS=2 18 | - LUAROCKS_VER=3.3.0 19 | matrix: 20 | - OPENRESTY_VER=1.15.8.3 21 | - OPENRESTY_VER=1.17.8.2 22 | 23 | install: 24 | - mkdir -p download-cache 25 | - if [ -z "$OPENRESTY_VER" ]; then export OPENRESTY_VER=1.15.8.3; fi 26 | - if [ ! -f download-cache/openresty-$OPENRESTY_VER.tar.gz ]; then wget -O download-cache/openresty-$OPENRESTY_VER.tar.gz http://openresty.org/download/openresty-$OPENRESTY_VER.tar.gz; fi 27 | - if [ ! -f download-cache/luarocks-$LUAROCKS_VER.tar.gz ]; then wget -O download-cache/luarocks-$LUAROCKS_VER.tar.gz https://luarocks.github.io/luarocks/releases/luarocks-$LUAROCKS_VER.tar.gz; fi 28 | - if [ ! -f download-cache/cpanm ]; then wget -O download-cache/cpanm https://cpanmin.us/; fi 29 | - chmod +x download-cache/cpanm 30 | - download-cache/cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1) 31 | - download-cache/cpanm --notest --local-lib=$TRAVIS_BUILD_DIR/perl5 local::lib && eval $(perl -I $TRAVIS_BUILD_DIR/perl5/lib/perl5/ -Mlocal::lib) 32 | - tar -zxf download-cache/openresty-$OPENRESTY_VER.tar.gz 33 | - tar -zxf download-cache/luarocks-$LUAROCKS_VER.tar.gz 34 | - pushd openresty-$OPENRESTY_VER 35 | - export OPENRESTY_PREFIX=$TRAVIS_BUILD_DIR/openresty-$OPENRESTY_VER 36 | - ./configure --prefix=$OPENRESTY_PREFIX --without-http_ssl_module -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 37 | - make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 38 | - make install > build.log 2>&1 || (cat build.log && exit 1) 39 | - popd 40 | - pushd luarocks-$LUAROCKS_VER 41 | - export LUAROCKS_PREFIX=$TRAVIS_BUILD_DIR/luarocks-$LUAROCKS_VER 42 | - ./configure --with-lua=$OPENRESTY_PREFIX/luajit --with-lua-include=$OPENRESTY_PREFIX/luajit/include/luajit-2.1 --lua-suffix=jit 43 | - make build 44 | - sudo make install 45 | - popd 46 | - export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/bin:$LUAROCKS_PREFIX/bin:$PATH 47 | - sudo luarocks install luacheck > build.log 2>&1 || (cat build.log && exit 1) 48 | - sudo luarocks install busted > build.log 2>&1 || (cat build.log && exit 1) 49 | - sudo luarocks make 50 | - luarocks --version 51 | - nginx -V 52 | 53 | script: 54 | - luacheck ./src 55 | - busted 56 | - TEST_NGINX_RANDOMIZE=1 prove -v -r t 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016-2022 Kong Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | Lua library containing a dns client, several utilities, and a load-balancer. 5 | 6 | The module is currently OpenResty only, and builds on top of the 7 | [`lua-resty-dns`](https://github.com/openresty/lua-resty-dns) library 8 | 9 | Features 10 | ======== 11 | 12 | - resolves A, AAAA, CNAME and SRV records, including port 13 | - parses `/etc/hosts` 14 | - parses `/resolv.conf` and applies `LOCALDOMAIN` and `RES_OPTIONS` variables 15 | - caches dns query results in memory 16 | - synchronizes requests (a single request for many requestors, eg. when cached ttl expires under heavy load) 17 | - `toip` applies a local (weighted) round-robin scheme on the query results 18 | - (weighted) round-robin balancer 19 | - consistent-hashing balancer 20 | - least-connections balancer 21 | 22 | 23 | Copyright and license 24 | ===================== 25 | 26 | Copyright: (c) 2016-2021 Kong, Inc. 27 | 28 | Author: Thijs Schreijer 29 | 30 | License: [Apache 2.0](https://opensource.org/licenses/Apache-2.0) 31 | 32 | Testing 33 | ======= 34 | 35 | Tests are executed using `busted`, but because they run inside the `resty` cli tool, you must 36 | use the `rbusted` script. 37 | 38 | For troubleshooting purposes: see the `/extra` folder for how to parse logs 39 | 40 | History 41 | ======= 42 | 43 | Versioning is strictly based on [Semantic Versioning](https://semver.org/) 44 | 45 | Release process: 46 | 47 | 1. update the changelog below 48 | 2. update the rockspec file 49 | 3. generate the docs using `ldoc .` 50 | 4. commit and tag the release 51 | 5. upload rock to LuaRocks 52 | 53 | ### 6.0.2 (05-Jul-2021) 54 | 55 | - Fix: `validTtl` should not be used for host-file entries. 56 | [PR 134](https://github.com/Kong/lua-resty-dns-client/pull/134) 57 | 58 | ### 6.0.1 (22-Jun-2021) 59 | 60 | - Performance: reduce amount of timers on init_worker. [PR 130](https://github.com/Kong/lua-resty-dns-client/pull/130) 61 | 62 | ### 6.0.0 (05-Apr-2021) 63 | 64 | - BREAKING: the round-robin balancing algorithm is now implemented in 65 | `resty.dns.round_robin` and has no consistent-hashing algorithm features, 66 | which are now all restricted to `resty.dns.consistent_hashing`. 67 | [PR 123](https://github.com/Kong/lua-resty-dns-client/pull/123) 68 | - Added: option to enable or disable nameservers randomization. Note: this 69 | feature depends on 70 | [the to-be-released feature](https://github.com/openresty/lua-resty-dns/commit/ad4a51c8cae8c3fb8f712fa91fda660ab8a89669) 71 | in [lua-resty-dns](https://github.com/openresty/lua-resty-dns). 72 | [PR 119](https://github.com/Kong/lua-resty-dns-client/pull/119) 73 | 74 | 75 | ### 5.2.3 (19-Mar-2021) 76 | 77 | - Fix: potential synchronisation issue in the least-connections balancer. 78 | [PR 126](https://github.com/Kong/lua-resty-dns-client/pull/126) 79 | 80 | ### 5.2.2 (11-Mar-2021) 81 | 82 | - Fix: do not iterate over all the search domains when resolving an unambiguous 83 | fully-qualified domain name (FQDN), i.e. ended in a dot. 84 | [PR 122](https://github.com/Kong/lua-resty-dns-client/pull/122) 85 | 86 | ### 5.2.1 (21-Jan-2021) 87 | 88 | - Fix: balancer DNS updates could go into a busy loop upon renewal. Reported as 89 | [Kong issue #6739](https://github.com/Kong/kong/issues/6739), 90 | fixed with [PR 116](https://github.com/Kong/lua-resty-dns-client/pull/116). 91 | 92 | ### 5.2.0 (7-Jan-2021) 93 | 94 | - Fix: now a single timer is used to check for expired records instead of one 95 | per host, significantly reducing the number of resources required for DNS 96 | resolution. [PR 112](https://github.com/Kong/lua-resty-dns-client/pull/112) 97 | 98 | ### 5.1.1 (7-Oct-2020) 99 | 100 | - Dependency: Bump lua-resty-timer to 1.0 101 | 102 | ### 5.1.0 (28-Sep-2020) 103 | 104 | - Fix: workaround for LuaJIT/ARM bug, see [Issue 93](https://github.com/Kong/lua-resty-dns-client/issues/93). 105 | - Fix: table reduction was calculated wrong. Not a "functional" bug, just causing 106 | slightly less agressive memory releasing. 107 | - Added: alternative implementation of the consistent-hashing balancing algorithm, 108 | which does not rely on the addresses addition and removal order to build the 109 | same request distribution among different instances. See 110 | [PR 97](https://github.com/Kong/lua-resty-dns-client/pull/97). 111 | 112 | ### 5.0.0 (14-May-2020) 113 | 114 | - BREAKING: `getPeer` now returns the host-header value instead of the hostname 115 | that was used to add the address. This is only breaking if a host was added through 116 | `addHost` with an ip-address. In that case `getPeer` will no longer return the 117 | ip-address as the hostname, but will now return `nil`. See 118 | [PR 89](https://github.com/Kong/lua-resty-dns-client/pull/89). 119 | - Added: option `useSRVname`, if truthy then `getPeer` will return the name as found 120 | in the SRV record, instead of the hostname as added to the balancer. 121 | See [PR 89](https://github.com/Kong/lua-resty-dns-client/pull/89). 122 | - Added: callback return an extra parameter; the host-header for the address added/removed. 123 | - Fix: using the module instance instead of the passed one for dns resolution 124 | in the balancer (only affected testing). See [PR 88](https://github.com/Kong/lua-resty-dns-client/pull/88). 125 | 126 | ### 4.2.0 (23-Mar-2020) 127 | 128 | - Change: export DNS source type on status report. See [PR 86](https://github.com/Kong/lua-resty-dns-client/pull/86). 129 | 130 | ### 4.1.3 (24-Jan-2020) 131 | 132 | - Fix: fix ttl-0 records issues with the balancer, see Kong issue 133 | https://github.com/Kong/kong/issues/5477 134 | * the previous record was not properly detected as a ttl=0 record 135 | by checking on the `__ttl0flag` we now do 136 | * since the "fake" SRV record wasn't updated with a new expiry 137 | time the expiry-check-timer would keep updating that record 138 | every second 139 | 140 | ### 4.1.2 (10-Dec-2019) 141 | 142 | - Fix: handle cases when `lastQuery` is `nil`, see [PR 81](https://github.com/Kong/lua-resty-dns-client/pull/81) 143 | and [PR 82](https://github.com/Kong/lua-resty-dns-client/pull/82). 144 | 145 | ### 4.1.1 (14-Nov-2019) 146 | 147 | - Fix: added logging of try-list to the TCP/UDP wrappers, see [PR 75](https://github.com/Kong/lua-resty-dns-client/pull/75). 148 | - Fix: reduce logging noise of the requery timer 149 | 150 | ### 4.1.0 (7-Aug-2019) 151 | 152 | - Fix: unhealthy balancers would not recover because they would not refresh the 153 | DNS records used. See [PR 73](https://github.com/Kong/lua-resty-dns-client/pull/73). 154 | - Added: automatic background resolving of hostnames, expiry will be checked 155 | every second, and if needed DNS (and balancer) will be updated. See [PR 73](https://github.com/Kong/lua-resty-dns-client/pull/73). 156 | 157 | ### 4.0.0 (26-Jun-2019) 158 | 159 | - BREAKING: the balancer callback is called with a new event; "health" whenever 160 | the health status of the balancer changes. 161 | - BREAKING: renamed `setPeerStatus` to `setAddressStatus` to be in line with the 162 | new `setHostStatus`, and prevent confusion. 163 | - Added: keep track of unavailable weight. Added the `getStatus` method to 164 | return health, of the entire balancer structure. Health itself is determined 165 | based on the new property `healthThreshold`. 166 | - Added: prevention of cascading failures when balancer is unhealthy. Use the 167 | `healthThreshold` value to set when the balancer is considered unhealthy. 168 | - Added: method `setHostStatus`, to set the availability/health state of all 169 | addresses belonging to a host at once. 170 | - Fix: when an asyncquery failed to create the timer, it would silently ignore 171 | the error. Error is now being logged. 172 | 173 | ### 3.0.2 (8-Mar-2019) Bugfix 174 | 175 | - Fix: callback for adding an address did not pass the address object, but 176 | instead passed the balancer object twice. 177 | 178 | ### 3.0.1 (5-Mar-2019) Bugfix 179 | 180 | - Fix: "balancer is nil" error, see issue #49. 181 | 182 | ### 3.0.0 (7-Nov-2018) Refactor & least-connections balancer 183 | 184 | - Refactor: split the balancer in a base class (handling DNS resolution) and 185 | the ring-balancer, implementing the algorithm. 186 | - Added: new least-connections balancer 187 | - Fix: since addresses could occasionally hold names instead of IP addresses, 188 | it could happen that a call to `setPeerStatus` was unsuccessful, because the 189 | IP address would not match the name in the `address` object. Now a 190 | `handle` is returned by `getPeer`. 191 | - BREAKING: `getPeer` signature (and return values) changed, making this a 192 | breaking change. 193 | 194 | ### 2.2.0 (28-Aug-2018) Fixes and a new option 195 | 196 | - Added: a new option `validTtl` that, if set, will forcefully override the 197 | `ttl` value of any valid answer received. [Issue 48](https://github.com/Kong/lua-resty-dns-client/issues/48). 198 | - Fix: remove multiline log entries, now encoded as single-line json. [Issue 52](https://github.com/Kong/lua-resty-dns-client/issues/52). 199 | - Fix: always inject a `localhost` value, even if not in `/etc/hosts`. [Issue 54](https://github.com/Kong/lua-resty-dns-client/issues/54). 200 | - Fix: added a workaround for Amazon Route 53 nameservers replying with a 201 | `ttl=0` whilst the record has a non-0 ttl. [Issue 56](https://github.com/Kong/lua-resty-dns-client/issues/56). 202 | 203 | ### 2.1.0 (21-May-2018) Fixes 204 | 205 | - Fix: the round-robin scheme for the balancer starts at a randomized position 206 | to prevent all workers from starting with the same peer. 207 | - Fix: the balancer no longer returns `port = 0` for SRV records without a 208 | port, the default port is now returned. 209 | - Fix: ipv6 nameservers with a scope in their address are not supported. This 210 | fix will simply skip them instead of throwing errors upon resolving. Fixes 211 | [issue 43](https://github.com/Kong/lua-resty-dns-client/issues/43). 212 | - Minor: improved logging in the balancer 213 | - Minor: relax requery default interval for failed dns queries from 1 to 30 214 | seconds. 215 | 216 | ### 2.0.0 (22-Feb-2018) Major performance improvement (balancer) and bugfixes 217 | 218 | - BREAKING: improved performance and memory footprint for large balancers. 219 | 80-85% less memory will be used, while creation time dropped by 85-90%. Since 220 | the `host:getPeer()` function signature changed, this is a breaking change. 221 | - Change: BREAKING the errors for cache-only lookup failures and empty records 222 | have been changed. 223 | - Fix: do not fail initialization without nameservers. 224 | - Fix: properly recognize IPv6 in square brackets from the /etc/hosts file. 225 | - Fix: do not set success-type to types we're not looking for. Fixes 226 | [Kong issue #3210](https://github.com/Kong/kong/issues/3210). 227 | - Fix: store records from the additional section in cache 228 | - Fix: do not overwrite stale data in the client cache with empty records 229 | 230 | ### 1.0.0 (14-Dec-2017) Fixes and IPv6 231 | 232 | - Change: BREAKING all IPv6 addresses are now returned with square brackets 233 | - Fix: properly recognize IPv6 addresses in square brackets 234 | 235 | ### 0.6.3 (27-Nov-2017) Fixes and flagging unhealthy peers 236 | 237 | - Added: flag to mark an address as failed/unhealthy, see `setPeerStatus` 238 | - Added: callback to receive balancer updates; addresses added-to/removed-from 239 | the balancer (after DNS updates for example). 240 | - fix: SRV record entries with a weight 0 are now supported 241 | - fix: failure of the last hostname to resolve (balancer) 242 | 243 | ### 0.6.2 (04-Sep-2017) Fixes and refactor 244 | 245 | - Fix: balancer not returning hostname for named SRV entries. See 246 | [issue #17](https://github.com/Kong/lua-resty-dns-client/issues/17) 247 | - Fix: fix an occasionally failing test 248 | - Refactor: remove metadata from the records, instead store it in its own cache 249 | 250 | ### 0.6.1 (28-Jul-2017) Randomization adjusted 251 | 252 | - Change: use a different randomizer for the ring-balancer to predictably 253 | recreate the balancer in the exact same state (adds the `lrandom` library as 254 | a new dependency) 255 | 256 | ### 0.6.0 (14-Jun-2017) Rewritten resolver core to resolve async 257 | 258 | - Added: resolution will be done async whenever possible. For this to work a new 259 | setting has been introduced `staleTtl` which determines for how long stale 260 | records will returned while a query is in progress in the background. 261 | - Change: BREAKING! several functions that previously returned and took a 262 | resolver object no longer do so. 263 | - Fix: no longer lookup ip adresses as names if the query type is not A or AAAA 264 | - Fix: normalize names to lowercase after query 265 | - Fix: set last-success types for hosts-file entries and ip-addresses 266 | 267 | ### 0.5.0 (25-Apr-2017) implement SEARCH and NDOTS 268 | 269 | - Removed: BREAKING! stdError function removed. 270 | - Added: implemented the `search` and `ndots` options. 271 | - Change: `resolve` no longer returns empty results or dns errors as a table 272 | but as lua errors (`nil + error`). 273 | - Change: `toip()` and `resolve()` have an extra result; history. A table with 274 | the list of tried names/types/results. 275 | - Fix: timeout and retrans options from `resolv.conf` were ignored by the 276 | `client` module. 277 | - Fix: nameservers with an ipv6 address would not be used properly. Also 278 | added a flag `enable_ipv6` (default == `false`) to enable the usage of 279 | ipv6 nameservers. 280 | 281 | ### 0.4.1 (21-Apr-2017) Bugfix 282 | 283 | - Fix: cname record caching causing excessive dns queries, 284 | see [Kong issue #2303](https://github.com/Kong/kong/issues/2303). 285 | 286 | ### 0.4.0 (30-Mar-2017) Bugfixes 287 | 288 | - Change: BREAKING! modified hash treatment, must now be an integer > 0 289 | - Added: BREAKING! a retry counter to fall-through on hashed-retries (changes 290 | the `getpeer` signature) 291 | - Fix: the MAXNS (3) was not honoured, so more than 3 nameservers would be parsed 292 | from the `resolv.conf` file. Fixes [Kong issue #2290](https://github.com/Kong/kong/issues/2290). 293 | - Added: two convenience hash functions 294 | - Performance: some improvements (pre-allocated tables for the slot lists) 295 | 296 | ### 0.3.2 (6-Mar-2017) Bugfixes 297 | 298 | - Fix: Cleanup disabled addresses but did not delete them, causing errors when 299 | they were repeatedly added/removed 300 | - Fix: potential racecondition when re-querying dns records 301 | - Fix: potential memoryleak when a balancer object was released with a running timer 302 | 303 | ### 0.3.1 (22-Feb-2017) Bugfixes 304 | 305 | - Kubernetes dns returns an SRV record for individual nodes, where the target 306 | is the same name again (hence causing a recursive loop). Now those entries 307 | will be removed, and if nothing is left, it will fail the SRV lookup, causing 308 | a fall-through to the next record type. 309 | - Kubernetes tends to return a port of 0 if none is provided/set, hence the 310 | `toip()` function now ignores a `port=0` and falls back on the port passed 311 | in. 312 | 313 | ### 0.3.0 (8-Nov-2016) Major breaking update 314 | 315 | - breaking: renamed a lot of things; method names, module names, etc. pretty 316 | much breaks everything... also releasing under a new name 317 | - feature: udp function `setpeername` added (client) 318 | - fix: do not synchronize dns queries for ttl=0 requests (client) 319 | - fix: full test coverage and accompanying fixes (ring-balancer) 320 | - feature: auto-retry for failed dns queries (ring-balancer) 321 | - feature: updating weights is now supported without removing/re-adding (ring-balancer) 322 | - change: auto-retry interval configurable for failed dns queries (ring-balancer) 323 | - change: max life-time interval configurable for ttl=0 dns records (ring-balancer) 324 | 325 | ### 0.2.1 (24-Oct-2016) Bugfix 326 | 327 | - fix: `toip()` failed on SRV records with only 1 entry 328 | 329 | ### 0.2 (18-Oct-2016) Added the balancer 330 | 331 | - fix: was creating resolver objects even if serving from cache 332 | - change: change resolver order (SRV is now first by default) for dns servers that create both SRV and A records for each entry 333 | - feature: make resolver order configurable 334 | - feature: ring-balancer (experimental, no full test coverage yet) 335 | - other: more test coverage for the dns client 336 | 337 | ### 0.1 (09-Sep-2016) Initial released version 338 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | project='lua-resty-dns-client' 2 | title='DNS client for OpenResty' 3 | description='DNS client, utilities, and load balancers for OpenResty' 4 | format='discount' 5 | file='./src/' 6 | dir='docs' 7 | readme='README.md' 8 | sort=true 9 | sort_modules=true 10 | all=false 11 | style='./docs/' 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 48 | 49 |
50 | 51 | 52 |

DNS client, utilities, and load balancers for OpenResty

53 | 54 |

Modules

55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
resty.dns.balancer.baseBase-balancer.
resty.dns.balancer.consistent_hashingConsistent-Hashing balancer

63 | 64 |

This balancer implements a consistent-hashing algorithm based on the 65 | Ketama algorithm.

resty.dns.balancer.handleHandle module.
resty.dns.balancer.least_connectionsLeast-connections balancer.
resty.dns.balancer.round_robinRound-Robin balancer
resty.dns.clientDNS client.
resty.dns.utilsDNS utility module.
88 |

Topics

89 | 90 | 91 | 92 | 93 | 94 |
README.md
95 | 96 |
97 |
98 |
99 | generated by LDoc 1.4.6 100 | Last updated 2021-07-06 11:55:24 101 |
102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/ldoc.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #47555c; 3 | font-size: 16px; 4 | font-family: "Open Sans", sans-serif; 5 | margin: 0; 6 | background: #eff4ff; 7 | } 8 | 9 | a:link { color: #008fee; } 10 | a:visited { color: #008fee; } 11 | a:hover { color: #22a7ff; } 12 | 13 | h1 { font-size:26px; font-weight: normal; } 14 | h2 { font-size:22px; font-weight: normal; } 15 | h3 { font-size:18px; font-weight: normal; } 16 | h4 { font-size:16px; font-weight: bold; } 17 | 18 | hr { 19 | height: 1px; 20 | background: #c1cce4; 21 | border: 0px; 22 | margin: 15px 0; 23 | } 24 | 25 | code, tt { 26 | font-family: monospace; 27 | } 28 | span.parameter { 29 | font-family: monospace; 30 | font-weight: bold; 31 | color: rgb(99, 115, 131); 32 | } 33 | span.parameter:after { 34 | content:":"; 35 | } 36 | span.types:before { 37 | content:"("; 38 | } 39 | span.types:after { 40 | content:")"; 41 | } 42 | .type { 43 | font-weight: bold; font-style:italic 44 | } 45 | 46 | p.name { 47 | font-family: "Andale Mono", monospace; 48 | } 49 | 50 | #navigation { 51 | float: left; 52 | background-color: white; 53 | border-right: 1px solid #d3dbec; 54 | border-bottom: 1px solid #d3dbec; 55 | 56 | width: 14em; 57 | vertical-align: top; 58 | overflow: visible; 59 | } 60 | 61 | #navigation br { 62 | display: none; 63 | } 64 | 65 | #navigation h1 { 66 | background-color: white; 67 | border-bottom: 1px solid #d3dbec; 68 | padding: 15px; 69 | margin-top: 0px; 70 | margin-bottom: 0px; 71 | } 72 | 73 | #navigation h2 { 74 | font-size: 18px; 75 | background-color: white; 76 | border-bottom: 1px solid #d3dbec; 77 | padding-left: 15px; 78 | padding-right: 15px; 79 | padding-top: 10px; 80 | padding-bottom: 10px; 81 | margin-top: 30px; 82 | margin-bottom: 0px; 83 | } 84 | 85 | #content h1 { 86 | background-color: #2c3e67; 87 | color: white; 88 | padding: 15px; 89 | margin: 0px; 90 | } 91 | 92 | #content h2 { 93 | background-color: #6c7ea7; 94 | color: white; 95 | padding: 15px; 96 | padding-top: 15px; 97 | padding-bottom: 15px; 98 | margin-top: 0px; 99 | } 100 | 101 | #content h2 a { 102 | background-color: #6c7ea7; 103 | color: white; 104 | text-decoration: none; 105 | } 106 | 107 | #content h2 a:hover { 108 | text-decoration: underline; 109 | } 110 | 111 | #content h3 { 112 | font-style: italic; 113 | padding-top: 15px; 114 | padding-bottom: 4px; 115 | margin-right: 15px; 116 | margin-left: 15px; 117 | margin-bottom: 5px; 118 | border-bottom: solid 1px #bcd; 119 | } 120 | 121 | #content h4 { 122 | margin-right: 15px; 123 | margin-left: 15px; 124 | border-bottom: solid 1px #bcd; 125 | } 126 | 127 | #content pre { 128 | margin: 15px; 129 | } 130 | 131 | pre { 132 | background-color: rgb(50, 55, 68); 133 | color: white; 134 | border-radius: 3px; 135 | /* border: 1px solid #C0C0C0; /* silver */ 136 | padding: 15px; 137 | overflow: auto; 138 | font-family: "Andale Mono", monospace; 139 | } 140 | 141 | #content ul pre.example { 142 | margin-left: 0px; 143 | } 144 | 145 | table.index { 146 | /* border: 1px #00007f; */ 147 | } 148 | table.index td { text-align: left; vertical-align: top; } 149 | 150 | #navigation ul 151 | { 152 | font-size:1em; 153 | list-style-type: none; 154 | margin: 1px 1px 10px 1px; 155 | padding-left: 20px; 156 | } 157 | 158 | #navigation li { 159 | text-indent: -1em; 160 | display: block; 161 | margin: 3px 0px 0px 22px; 162 | } 163 | 164 | #navigation li li a { 165 | margin: 0px 3px 0px -1em; 166 | } 167 | 168 | #content { 169 | margin-left: 14em; 170 | } 171 | 172 | #content p { 173 | padding-left: 15px; 174 | padding-right: 15px; 175 | } 176 | 177 | #content table { 178 | padding-left: 15px; 179 | padding-right: 15px; 180 | background-color: white; 181 | } 182 | 183 | #content p, #content table, #content ol, #content ul, #content dl { 184 | max-width: 900px; 185 | } 186 | 187 | #about { 188 | padding: 15px; 189 | padding-left: 16em; 190 | background-color: white; 191 | border-top: 1px solid #d3dbec; 192 | border-bottom: 1px solid #d3dbec; 193 | } 194 | 195 | table.module_list, table.function_list { 196 | border-width: 1px; 197 | border-style: solid; 198 | border-color: #cccccc; 199 | border-collapse: collapse; 200 | margin: 15px; 201 | } 202 | table.module_list td, table.function_list td { 203 | border-width: 1px; 204 | padding-left: 10px; 205 | padding-right: 10px; 206 | padding-top: 5px; 207 | padding-bottom: 5px; 208 | border: solid 1px rgb(193, 204, 228); 209 | } 210 | table.module_list td.name, table.function_list td.name { 211 | background-color: white; min-width: 200px; border-right-width: 0px; 212 | } 213 | table.module_list td.summary, table.function_list td.summary { 214 | background-color: white; width: 100%; border-left-width: 0px; 215 | } 216 | 217 | dl.function { 218 | margin-right: 15px; 219 | margin-left: 15px; 220 | border-bottom: solid 1px rgb(193, 204, 228); 221 | border-left: solid 1px rgb(193, 204, 228); 222 | border-right: solid 1px rgb(193, 204, 228); 223 | background-color: white; 224 | } 225 | 226 | dl.function dt { 227 | color: rgb(99, 123, 188); 228 | font-family: monospace; 229 | border-top: solid 1px rgb(193, 204, 228); 230 | padding: 15px; 231 | } 232 | 233 | dl.function dd { 234 | margin-left: 15px; 235 | margin-right: 15px; 236 | margin-top: 5px; 237 | margin-bottom: 15px; 238 | } 239 | 240 | #content dl.function dd h3 { 241 | margin-top: 0px; 242 | margin-left: 0px; 243 | padding-left: 0px; 244 | font-size: 16px; 245 | color: rgb(128, 128, 128); 246 | border-bottom: solid 1px #def; 247 | } 248 | 249 | #content dl.function dd ul, #content dl.function dd ol { 250 | padding: 0px; 251 | padding-left: 15px; 252 | list-style-type: none; 253 | } 254 | 255 | ul.nowrap { 256 | overflow:auto; 257 | white-space:nowrap; 258 | } 259 | 260 | .section-description { 261 | padding-left: 15px; 262 | padding-right: 15px; 263 | } 264 | 265 | /* stop sublists from having initial vertical space */ 266 | ul ul { margin-top: 0px; } 267 | ol ul { margin-top: 0px; } 268 | ol ol { margin-top: 0px; } 269 | ul ol { margin-top: 0px; } 270 | 271 | /* make the target distinct; helps when we're navigating to a function */ 272 | a:target + * { 273 | background-color: #FF9; 274 | } 275 | 276 | 277 | /* styles for prettification of source */ 278 | pre .comment { color: #bbccaa; } 279 | pre .constant { color: #a8660d; } 280 | pre .escape { color: #844631; } 281 | pre .keyword { color: #ffc090; font-weight: bold; } 282 | pre .library { color: #0e7c6b; } 283 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 284 | pre .string { color: #8080ff; } 285 | pre .number { color: #f8660d; } 286 | pre .operator { color: #2239a8; font-weight: bold; } 287 | pre .preprocessor, pre .prepro { color: #a33243; } 288 | pre .global { color: #c040c0; } 289 | pre .user-keyword { color: #800080; } 290 | pre .prompt { color: #558817; } 291 | pre .url { color: #272fc2; text-decoration: underline; } 292 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.balancer.consistent_hashing.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 55 | 56 |
57 | 58 |

Module resty.dns.balancer.consistent_hashing

59 |

Consistent-Hashing balancer

60 | 61 |

This balancer implements a consistent-hashing algorithm based on the 62 | Ketama algorithm.

63 |

This load balancer is designed to make sure that every time a load 64 | balancer object is built, it is built the same, no matter the order the 65 | process is done.

66 | 67 |

NOTE: This documentation only described the altered user 68 | methods/properties, see the user properties from the balancer_base 69 | for a complete overview.

70 |

Info:

71 |
    72 |
  • Copyright: 2020 Kong Inc. All rights reserved.
  • 73 |
  • License: Apache 2.0
  • 74 |
  • Author: Vinicius Mignot
  • 75 |
76 | 77 | 78 |

Functions

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 |
_get_continuum ()for testing only
addHost (hostname, port, weight)Adds a host to the balancer.
afterHostUpdate (host)Actually adds the addresses to the continuum.
getPeer (cacheOnly, handle, valueToHash)Gets an IP/port/hostname combo for the value to hash 95 | This function will hash the valueToHash param and use it as an index 96 | in the continuum.
new (opts)Creates a new balancer.
103 | 104 |
105 |
106 | 107 | 108 |

Functions

109 | 110 |
111 |
112 | 113 | _get_continuum () 114 |
115 |
116 | for testing only 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 |
126 | 127 | addHost (hostname, port, weight) 128 |
129 |
130 | Adds a host to the balancer. 131 | This function checks if there is enough points to add more hosts and 132 | then call the base class's addHost(). 133 | see addHost() from the balancer_base for more details. 134 | 135 | 136 |

Parameters:

137 |
    138 |
  • hostname 139 | 140 | 141 | 142 |
  • 143 |
  • port 144 | 145 | 146 | 147 |
  • 148 |
  • weight 149 | 150 | 151 | 152 |
  • 153 |
154 | 155 | 156 | 157 | 158 | 159 |
160 |
161 | 162 | afterHostUpdate (host) 163 |
164 |
165 | Actually adds the addresses to the continuum. 166 | This function should not be called directly, as it will called by 167 | addHost() after adding the new host. 168 | This function makes sure the continuum will be built identically every 169 | time, no matter the order the hosts are added. 170 | 171 | 172 |

Parameters:

173 |
    174 |
  • host 175 | 176 | 177 | 178 |
  • 179 |
180 | 181 | 182 | 183 | 184 | 185 |
186 |
187 | 188 | getPeer (cacheOnly, handle, valueToHash) 189 |
190 |
191 | Gets an IP/port/hostname combo for the value to hash 192 | This function will hash the valueToHash param and use it as an index 193 | in the continuum. It will return the address that is at the hashed 194 | value or the first one found going counter-clockwise in the continuum. 195 | 196 | 197 |

Parameters:

198 |
    199 |
  • cacheOnly 200 | If truthy, no dns lookups will be done, only cache. 201 |
  • 202 |
  • handle 203 | the handle returned by a previous call to getPeer. 204 | This will retain some state over retries. See also setAddressStatus. 205 |
  • 206 |
  • valueToHash 207 | value for consistent hashing. Please note that this 208 | value will be hashed, so no need to hash it prior to calling this 209 | function. 210 |
  • 211 |
212 | 213 |

Returns:

214 |
    215 | 216 | ip + port + hostheader + handle, or nil+error 217 |
218 | 219 | 220 | 221 | 222 |
223 |
224 | 225 | new (opts) 226 |
227 |
228 | 229 |

Creates a new balancer.

230 | 231 |

The balancer is based on a wheel (continuum) with a number of points 232 | between MINCONTINUUMSIZE and MAXCONTINUUMSIZE points. Key points 233 | will be assigned to addresses based on their IP and port. The number 234 | of points each address will be assigned is proportional to their weight.

235 | 236 |

The options table has the following fields, additional to the ones from 237 | the balancer_base:

238 | 239 |
    240 |
  • hosts (optional) containing hostnames, ports, and weights. If 241 | omitted, ports and weights default respectively to 80 and 10. The list 242 | will be sorted before being added, so the order of entry is 243 | deterministic.
  • 244 |
  • wheelSize (optional) for total number of positions in the 245 | continuum. If omitted DEFAULT_CONTINUUM_SIZE is used. It is important 246 | to have enough indices to fit all addresses entries, keep in mind that 247 | each address will use 160 entries in the continuum (more or less, 248 | proportional to its weight, but the total points will always be 249 | 160 * addresses). Consider the maximum number of targets expected, as 250 | new hosts can be dynamically added, and DNS renewals might yield 251 | larger record sets. The wheelSize cannot be altered, the object has 252 | to built again to change this value. On a similar note, making it too 253 | big will have a performance impact to get peers from the continuum, as 254 | the values will be too dispersed among them.
  • 255 |
256 | 257 | 258 | 259 |

Parameters:

260 |
    261 |
  • opts 262 | table with options 263 |
  • 264 |
265 | 266 |

Returns:

267 |
    268 | 269 | new balancer object or nil+error 270 |
271 | 272 | 273 | 274 | 275 |
276 |
277 | 278 | 279 |
280 |
281 |
282 | generated by LDoc 1.4.6 283 | Last updated 2021-07-06 11:55:24 284 |
285 |
286 | 287 | 288 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.balancer.handle.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 55 | 56 |
57 | 58 |

Module resty.dns.balancer.handle

59 |

Handle module.

60 |

Implements handles to be used by the objBalancer:getPeer method. These 61 | implement a __gc method for tracking statistics and not leaking resources 62 | in case a connection gets aborted prematurely.

63 | 64 |

This module is only relevant when implementing your own balancer 65 | algorithms.

66 |

Info:

67 |
    68 |
  • Copyright: 2016-2020 Kong Inc. All rights reserved.
  • 69 |
  • License: Apache 2.0
  • 70 |
  • Author: Thijs Schreijer
  • 71 |
72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
get (__gc)Gets a handle from the cache.
release (handle)Returns a handle to the cache.
setCacheSize (size)Sets a new cache size.
89 | 90 |
91 |
92 | 93 | 94 |

Functions

95 | 96 |
97 |
98 | 99 | get (__gc) 100 |
101 |
102 | Gets a handle from the cache. 103 | The handle comes from the cache or it is newly created. A handle is just a 104 | table. It will have two special fields:

105 | 106 |
    107 |
  • __udata: (read-only) a userdata used to track the lifetime of the handle
  • 108 |
  • __gc: (read/write) this method will be called on GC.
  • 109 |
110 | 111 |

NOTE: the __gc will only be called when the handle is garbage collected, 112 | not when it is returned by calling release. 113 | 114 | 115 |

Parameters:

116 |
    117 |
  • __gc 118 | (optional, function) the method called when the handle is GC'ed. 119 |
  • 120 |
121 | 122 |

Returns:

123 |
    124 | 125 | handle 126 |
127 | 128 | 129 | 130 |

Usage:

131 |
    132 |
    local handle = _M
    133 | 
    134 | local my_gc_handler = function(self)
    135 |   print(self.name .. " was deleted")
    136 | end
    137 | 
    138 | local h1 = handle.get(my_gc_handler)
    139 | h1.name = "Obama"
    140 | local h2 = handle.get(my_gc_handler)
    141 | h2.name = "Trump"
    142 | 
    143 | handle.release(h1)   -- explicitly release it
    144 | h1 = nil
    145 | h2 = nil             -- not released, will be GC'ed
    146 | collectgarbage()
    147 | collectgarbage()     --> "Trump was deleted"
    148 |
149 | 150 |
151 |
152 | 153 | release (handle) 154 |
155 |
156 | Returns a handle to the cache. 157 | The handle will be cleared, returned to the cache, and its __gc handle 158 | will NOT be called. 159 | 160 | 161 |

Parameters:

162 |
    163 |
  • handle 164 | the handle to return to the cache 165 |
  • 166 |
167 | 168 |

Returns:

169 |
    170 | 171 | nothing 172 |
173 | 174 | 175 | 176 | 177 |
178 |
179 | 180 | setCacheSize (size) 181 |
182 |
183 | Sets a new cache size. The default size is 1000. 184 | 185 | 186 |

Parameters:

187 |
    188 |
  • size 189 | the new size. 190 |
  • 191 |
192 | 193 |

Returns:

194 |
    195 | 196 | nothing, or throws an error on bad input 197 |
198 | 199 | 200 | 201 | 202 |
203 |
204 | 205 | 206 |
207 |
208 |
209 | generated by LDoc 1.4.6 210 | Last updated 2021-07-06 11:55:24 211 |
212 |
213 | 214 | 215 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.balancer.least_connections.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 55 | 56 |
57 | 58 |

Module resty.dns.balancer.least_connections

59 |

Least-connections balancer.

60 |

This balancer implements a least-connections algorithm. The balancer will 61 | honour the weights. See the base-balancer for details on how the weights 62 | are set.

63 | 64 |

NOTE: This documentation only described the altered user methods/properties 65 | from the base-balancer. See the user properties from the balancer_base for a 66 | complete overview.

67 |

Info:

68 |
    69 |
  • Copyright: 2016-2020 Kong Inc. All rights reserved.
  • 70 |
  • License: Apache 2.0
  • 71 |
  • Author: Thijs Schreijer
  • 72 |
73 | 74 | 75 |

Functions

76 | 77 | 78 | 79 | 80 | 81 |
new (opts)Creates a new balancer.
82 | 83 |
84 |
85 | 86 | 87 |

Functions

88 | 89 |
90 |
91 | 92 | new (opts) 93 |
94 |
95 | 96 |

Creates a new balancer. The balancer is based on a binary heap tracking 97 | the number of active connections. The number of connections 98 | assigned will be relative to the weight.

99 | 100 |

The options table has the following fields, additional to the ones from 101 | the balancer_base;

102 | 103 |
    104 |
  • hosts (optional) containing hostnames, ports and weights. If omitted, 105 | ports and weights default respectively to 80 and 10.
  • 106 |
107 | 108 | 109 | 110 |

Parameters:

111 |
    112 |
  • opts 113 | table with options 114 |
  • 115 |
116 | 117 |

Returns:

118 |
    119 | 120 | new balancer object or nil+error 121 |
122 | 123 | 124 | 125 |

Usage:

126 |
    127 |
    -- hosts example
    128 | local hosts = {
    129 |   "konghq.com",                                      -- name only, as string
    130 |   { name = "github.com" },                           -- name only, as table
    131 |   { name = "getkong.org", port = 80, weight = 25 },  -- fully specified, as table
    132 | }
    133 |
134 | 135 |
136 |
137 | 138 | 139 |
140 |
141 |
142 | generated by LDoc 1.4.6 143 | Last updated 2021-07-06 11:55:24 144 |
145 |
146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.balancer.round_robin.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 51 | 52 |
53 | 54 |

Module resty.dns.balancer.round_robin

55 |

Round-Robin balancer

56 |

57 | 58 |

59 |

Info:

60 |
    61 |
  • Copyright: 2021 Kong Inc. All rights reserved.
  • 62 |
  • License: Apache 2.0
  • 63 |
  • Author: Vinicius Mignot
  • 64 |
65 | 66 | 67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 | generated by LDoc 1.4.6 78 | Last updated 2021-07-06 11:55:24 79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.client.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 56 | 57 |
58 | 59 |

Module resty.dns.client

60 |

DNS client.

61 |

62 | 63 | 64 |

Works with OpenResty only. Requires the lua-resty-dns module.

65 | 66 |

NOTES:

67 | 68 |
    69 |
  1. parsing the config files upon initialization uses blocking i/o, so use with 70 | care. See init for details.
  2. 71 |
  3. All returned records are directly from the cache. So do not modify them! 72 | If you need to, copy them first.
  4. 73 |
  5. TTL for records is the TTL returned by the server at the time of fetching 74 | and won't be updated while the client serves the records from its cache.
  6. 75 |
  7. resolving IPv4 (A-type) and IPv6 (AAAA-type) addresses is explicitly supported. If 76 | the hostname to be resolved is a valid IP address, it will be cached with a ttl of 77 | 10 years. So the user doesn't have to check for ip adresses.
  8. 78 |
79 |

80 |

Info:

81 |
    82 |
  • Copyright: 2016-2017 Kong Inc.
  • 83 |
  • License: Apache 2.0
  • 84 |
  • Author: Thijs Schreijer
  • 85 |
86 | 87 | 88 |

Resolving

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
init (options)Initialize the client.
resolve (qname, r_opts, dnsCacheOnly, try_list)Resolve a name.
toip (qname, port, dnsCacheOnly, try_list)Resolves to an IP and port number.
103 |

Socket functions

104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
connect (sock, host, port, opts)Implements tcp-connect method with dns resolution.
setpeername (sock, host, port)Implements udp-setpeername method with dns resolution.
114 | 115 |
116 |
117 | 118 | 119 |

Resolving

120 | 121 |
122 | When resolving names, queries will be synchronized, such that only a single 123 | query will be sent. If stale data is available, the request will return 124 | stale data immediately, whilst continuing to resolve the name in the 125 | background.

126 | 127 |

The dnsCacheOnly parameter found with resolve and toip can be used in 128 | contexts where the co-socket api is unavailable. When the flag is set 129 | only cached data is returned, but it will never use blocking io. 130 |

131 |
132 |
133 | 134 | init (options) 135 |
136 |
137 | Initialize the client. Can be called multiple times. When called again it 138 | will clear the cache. 139 | 140 | 141 |

Parameters:

142 |
    143 |
  • options 144 | Same table as the OpenResty dns resolver, 145 | with some extra fields explained in the example below. 146 |
  • 147 |
148 | 149 |

Returns:

150 |
    151 | 152 | true on success, nil+error, or throw an error on bad input 153 |
154 | 155 | 156 | 157 |

Usage:

158 |
    159 |
    -- config files to parse
    160 | -- hosts and resolvConf can both be a filename, or a table with file-contents
    161 | -- The contents of the hosts file will be inserted in the cache.
    162 | -- From resolv.conf the nameserver, search, ndots, attempts and timeout values will be used.
    163 | local hosts = {}  -- initialize without any blocking i/o
    164 | local resolvConf = {}  -- initialize without any blocking i/o
    165 | 
    166 | -- when getting nameservers from resolv.conf, get ipv6 servers?
    167 | local enable_ipv6 = false
    168 | 
    169 | -- Order in which to try different dns record types when resolving
    170 | -- 'last'; will try the last previously successful type for a hostname.
    171 | local order = { "last", "SRV", "A", "AAAA", "CNAME" }
    172 | 
    173 | -- Stale ttl for how long a stale record will be served from the cache
    174 | -- while a background lookup is in progress.
    175 | local staleTtl = 4.0    -- in seconds (can have fractions)
    176 | 
    177 | -- Cache ttl for empty and 'name error' (3) responses
    178 | local emptyTtl = 30.0   -- in seconds (can have fractions)
    179 | 
    180 | -- Cache ttl for other error responses
    181 | local badTtl = 1.0      -- in seconds (can have fractions)
    182 | 
    183 | -- Overriding ttl for valid queries, if given
    184 | local validTtl = nil    -- in seconds (can have fractions)
    185 | 
    186 | -- ndots, same as the resolv.conf option, if not given it is taken from
    187 | -- resolv.conf or otherwise set to 1
    188 | local ndots = 1
    189 | 
    190 | -- no_random, if set disables randomly picking the first nameserver, if not
    191 | -- given it is taken from resolv.conf option rotate (inverted).
    192 | -- Defaults to true.
    193 | local no_random = true
    194 | 
    195 | -- search, same as the resolv.conf option, if not given it is taken from
    196 | -- resolv.conf, or set to the domain option, or no search is performed
    197 | local search = {
    198 |   "mydomain.com",
    199 |   "site.domain.org",
    200 | }
    201 | 
    202 | -- Disables synchronization between queries, resulting in each lookup for the
    203 | -- same name being executed in it's own query to the nameservers. The default
    204 | -- (false) will synchronize multiple queries for the same name to a single
    205 | -- query to the nameserver.
    206 | noSynchronisation = false
    207 | 
    208 | assert(client.init({
    209 |          hosts = hosts,
    210 |          resolvConf = resolvConf,
    211 |          ndots = ndots,
    212 |          no_random = no_random,
    213 |          search = search,
    214 |          order = order,
    215 |          badTtl = badTtl,
    216 |          emptyTtl = emptTtl,
    217 |          staleTtl = staleTtl,
    218 |          validTtl = validTtl,
    219 |          enable_ipv6 = enable_ipv6,
    220 |          noSynchronisation = noSynchronisation,
    221 |        })
    222 | )
    223 |
224 | 225 |
226 |
227 | 228 | resolve (qname, r_opts, dnsCacheOnly, try_list) 229 |
230 |
231 | Resolve a name. 232 | If r_opts.qtype is given, then it will fetch that specific type only. If 233 | r_opts.qtype is not provided, then it will try to resolve 234 | the name using the record types, in the order as provided to init.

235 | 236 |

Note that unless explictly requesting a CNAME record (by setting r_opts.qtype) this 237 | function will dereference the CNAME records.

238 | 239 |

So requesting my.domain.com (assuming to be an AAAA record, and default order) will try to resolve 240 | it (the first time) as;

241 | 242 |
    243 |
  • SRV,
  • 244 |
  • then A,
  • 245 |
  • then AAAA (success),
  • 246 |
  • then CNAME (after AAAA success, this will not be tried)
  • 247 |
248 | 249 |

A second lookup will now try (assuming the cached entry expired);

250 | 251 |
    252 |
  • AAAA (as it was the last successful lookup),
  • 253 |
  • then SRV,
  • 254 |
  • then A,
  • 255 |
  • then CNAME.
  • 256 |
257 | 258 |

The outer loop will be based on the search and ndots options. Within each of 259 | those, the inner loop will be the query/record type. 260 | 261 | 262 |

Parameters:

263 |
    264 |
  • qname 265 | Name to resolve 266 |
  • 267 |
  • r_opts 268 | Options table, see remark about the qtype field above and 269 | OpenResty docs for more options. 270 |
  • 271 |
  • dnsCacheOnly 272 | Only check the cache, won't do server lookups 273 |
  • 274 |
  • try_list 275 | (optional) list of tries to add to 276 |
  • 277 |
278 | 279 |

Returns:

280 |
    281 | 282 | list of records + nil + try_list, or nil + err + try_list. 283 |
284 | 285 | 286 | 287 | 288 |
289 |
290 | 291 | toip (qname, port, dnsCacheOnly, try_list) 292 |
293 |
294 | Resolves to an IP and port number. 295 | Builds on top of resolve, but will also further dereference SRV type records.

296 | 297 |

When calling multiple times on cached records, it will apply load-balancing 298 | based on a round-robin (RR) scheme. For SRV records this will be a weighted 299 | round-robin (WRR) scheme (because of the weights it will be randomized). It will 300 | apply the round-robin schemes on each level 301 | individually.

302 | 303 |

Example;

304 | 305 |

SRV record for "my.domain.com", containing 2 entries (this is the 1st level);

306 | 307 |
    308 |
  • target = 127.0.0.1, port = 80, weight = 10
  • 309 |
  • target = "other.domain.com", port = 8080, weight = 5
  • 310 |
311 | 312 |

A record for "other.domain.com", containing 2 entries (this is the 2nd level);

313 | 314 |
    315 |
  • ip = 127.0.0.2
  • 316 |
  • ip = 127.0.0.3
  • 317 |
318 | 319 |

Now calling local ip, port = toip("my.domain.com", 123) in a row 6 times will result in;

320 | 321 |
    322 |
  • 127.0.0.1, 80
  • 323 |
  • 127.0.0.2, 8080 (port from SRV, 1st IP from A record)
  • 324 |
  • 127.0.0.1, 80 (completes WRR 1st level, 1st run)
  • 325 |
  • 127.0.0.3, 8080 (port from SRV, 2nd IP from A record, completes RR 2nd level)
  • 326 |
  • 127.0.0.1, 80
  • 327 |
  • 127.0.0.1, 80 (completes WRR 1st level, 2nd run, with different order as WRR is randomized)
  • 328 |
329 | 330 |

Debugging:

331 | 332 |

This function both takes and returns a try_list. This is an internal object 333 | representing the entire resolution history for a call. To prevent unnecessary 334 | string concatenations on a hot code path, it is not logged in this module. 335 | If you need to log it, just log tostring(try_list) from the caller code. 336 | 337 | 338 |

Parameters:

339 |
    340 |
  • qname 341 | hostname to resolve 342 |
  • 343 |
  • port 344 | (optional) default port number to return if none was found in 345 | the lookup chain (only SRV records carry port information, SRV with port=0 will be ignored) 346 |
  • 347 |
  • dnsCacheOnly 348 | Only check the cache, won't do server lookups (will 349 | not invalidate any ttl expired data and will hence possibly return expired data) 350 |
  • 351 |
  • try_list 352 | (optional) list of tries to add to 353 |
  • 354 |
355 | 356 |

Returns:

357 |
    358 | 359 | ip address + port + try_list, or in case of an error nil + error + try_list 360 |
361 | 362 | 363 | 364 | 365 |
366 |
367 |

Socket functions

368 | 369 |
370 |
371 | 372 | connect (sock, host, port, opts) 373 |
374 |
375 | Implements tcp-connect method with dns resolution. 376 | This builds on top of toip. If the name resolves to an SRV record, 377 | the port returned by the DNS server will override the one provided.

378 | 379 |

NOTE: can also be used for other connect methods, eg. http/redis 380 | clients, as long as the argument order is the same 381 | 382 | 383 |

Parameters:

384 |
    385 |
  • sock 386 | the tcp socket 387 |
  • 388 |
  • host 389 | hostname to connect to 390 |
  • 391 |
  • port 392 | port to connect to (will be overridden if toip returns a port) 393 |
  • 394 |
  • opts 395 | the options table 396 |
  • 397 |
398 | 399 |

Returns:

400 |
    401 | 402 | success, or nil + error 403 |
404 | 405 | 406 | 407 | 408 |
409 |
410 | 411 | setpeername (sock, host, port) 412 |
413 |
414 | Implements udp-setpeername method with dns resolution. 415 | This builds on top of toip. If the name resolves to an SRV record, 416 | the port returned by the DNS server will override the one provided. 417 | 418 | 419 |

Parameters:

420 |
    421 |
  • sock 422 | the udp socket 423 |
  • 424 |
  • host 425 | hostname to connect to 426 |
  • 427 |
  • port 428 | port to connect to (will be overridden if toip returns a port) 429 |
  • 430 |
431 | 432 |

Returns:

433 |
    434 | 435 | success, or nil + error 436 |
437 | 438 | 439 | 440 | 441 |
442 |
443 | 444 | 445 |
446 |
447 |
448 | generated by LDoc 1.4.6 449 | Last updated 2021-07-06 11:55:24 450 |
451 |
452 | 453 | 454 | -------------------------------------------------------------------------------- /docs/modules/resty.dns.utils.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | DNS client for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 58 | 59 |
60 | 61 |

Module resty.dns.utils

62 |

DNS utility module.

63 |

Parses the /etc/hosts and /etc/resolv.conf configuration files, caches them, 64 | and provides some utility functions.

65 | 66 |

NOTE: parsing the files is done using blocking i/o file operations.

67 |

Info:

68 |
    69 |
  • Copyright: 2016-2020 Kong Inc.
  • 70 |
  • License: Apache 2.0
  • 71 |
  • Author: Thijs Schreijer
  • 72 |
73 | 74 | 75 |

Fields

76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
DEFAULT_HOSTSDefault filename to parse for the hosts file.
DEFAULT_RESOLV_CONFDefault filename to parse for the resolv.conf file.
MAXNSMaximum number of nameservers to parse from the resolv.conf file
MAXSEARCHMaximum number of entries to parse from search parameter in the resolv.conf file
94 |

Parsing configuration files and variables

95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
applyEnv (config)Will parse LOCALDOMAIN and RES_OPTIONS environment variables.
parseHosts (filename)Parses a hosts file or table.
parseResolvConf (filename)Parses a resolv.conf file or table.
109 |

Caching configuration files and variables

110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
getHosts (ttl)returns the parseHosts results, but cached.
getResolv (ttl)returns the applyEnv results, but cached.
120 |

Miscellaneous

121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
hostnameType (name)checks the hostname type; ipv4, ipv6, or name.
parseHostname (name)parses a hostname with an optional port.
131 | 132 |
133 |
134 | 135 | 136 |

Fields

137 | 138 |
139 |
140 | 141 | DEFAULT_HOSTS 142 |
143 |
144 | Default filename to parse for the hosts file. 145 | 146 | 147 |
    148 |
  • DEFAULT_HOSTS 149 | Defaults to /etc/hosts 150 |
  • 151 |
152 | 153 | 154 | 155 | 156 | 157 |
158 |
159 | 160 | DEFAULT_RESOLV_CONF 161 |
162 |
163 | Default filename to parse for the resolv.conf file. 164 | 165 | 166 |
    167 |
  • DEFAULT_RESOLV_CONF 168 | Defaults to /etc/resolv.conf 169 |
  • 170 |
171 | 172 | 173 | 174 | 175 | 176 |
177 |
178 | 179 | MAXNS 180 |
181 |
182 | Maximum number of nameservers to parse from the resolv.conf file 183 | 184 | 185 |
    186 |
  • MAXNS 187 | Defaults to 3 188 |
  • 189 |
190 | 191 | 192 | 193 | 194 | 195 |
196 |
197 | 198 | MAXSEARCH 199 |
200 |
201 | Maximum number of entries to parse from search parameter in the resolv.conf file 202 | 203 | 204 |
    205 |
  • MAXSEARCH 206 | Defaults to 6 207 |
  • 208 |
209 | 210 | 211 | 212 | 213 | 214 |
215 |
216 |

Parsing configuration files and variables

217 | 218 |
219 |
220 | 221 | applyEnv (config) 222 |
223 |
224 | Will parse LOCALDOMAIN and RES_OPTIONS environment variables. 225 | It will insert them into the given resolv.conf based configuration table.

226 | 227 |

NOTE: if the input is nil+error it will return the input, to allow for 228 | pass-through error handling 229 | 230 | 231 |

Parameters:

232 |
    233 |
  • config 234 | Options table, as parsed by parseResolvConf, or an empty table to get only the environment options 235 |
  • 236 |
237 | 238 |

Returns:

239 |
    240 | 241 | modified table 242 |
243 | 244 | 245 |

See also:

246 | 249 | 250 |

Usage:

251 |
    252 |
    -- errors are passed through, so this;
    253 | local config, err = utils.parseResolvConf()
    254 | if config then
    255 |   config, err = utils.applyEnv(config)
    256 | end
    257 | 
    258 | -- Is identical to;
    259 | local config, err = utils.applyEnv(utils.parseResolvConf())
    260 |
261 | 262 |
263 |
264 | 265 | parseHosts (filename) 266 |
267 |
268 | Parses a hosts file or table. 269 | Does not check for correctness of ip addresses nor hostnames. Might return 270 | nil + error if the file cannot be read.

271 | 272 |

NOTE: All output will be normalized to lowercase, IPv6 addresses will 273 | always be returned in brackets. 274 | 275 | 276 |

Parameters:

277 |
    278 |
  • filename 279 | (optional) Filename to parse, or a table with the file 280 | contents in lines (defaults to '/etc/hosts' if omitted) 281 |
  • 282 |
283 | 284 |

Returns:

285 |
    286 |
  1. 287 | 1; reverse lookup table, ip addresses (table with ipv4 and ipv6 288 | fields) indexed by their canonical names and aliases
  2. 289 |
  3. 290 | 2; list with all entries. Containing fields ip, canonical and family, 291 | and a list of aliasses
  4. 292 |
293 | 294 | 295 | 296 |

Usage:

297 |
    298 |
    local lookup, list = utils.parseHosts({
    299 |   "127.0.0.1   localhost",
    300 |   "1.2.3.4     someserver",
    301 |   "192.168.1.2 test.computer.com",
    302 |   "192.168.1.3 ftp.COMPUTER.com alias1 alias2",
    303 | })
    304 | 
    305 | print(lookup["localhost"])         --> "127.0.0.1"
    306 | print(lookup["ftp.computer.com"])  --> "192.168.1.3" note: name in lowercase!
    307 | print(lookup["alias1"])            --> "192.168.1.3"
    308 |
309 | 310 |
311 |
312 | 313 | parseResolvConf (filename) 314 |
315 |
316 | Parses a resolv.conf file or table. 317 | Does not check for correctness of ip addresses nor hostnames, bad options 318 | will be ignored. Might return nil + error if the file cannot be read. 319 | 320 | 321 |

Parameters:

322 |
    323 |
  • filename 324 | (optional) File to parse (defaults to '/etc/resolv.conf' if 325 | omitted) or a table with the file contents in lines. 326 |
  • 327 |
328 | 329 |

Returns:

330 |
    331 | 332 | a table with fields nameserver (table), domain (string), search (table), 333 | sortlist (table) and options (table) 334 |
335 | 336 | 337 |

See also:

338 | 341 | 342 | 343 |
344 |
345 |

Caching configuration files and variables

346 | 347 |
348 |
349 | 350 | getHosts (ttl) 351 |
352 |
353 | returns the parseHosts results, but cached. 354 | Once ttl has been provided, only after it expires the file will be parsed again.

355 | 356 |

NOTE: if cached, the SAME tables will be returned, so do not modify them 357 | unless you know what you are doing! 358 | 359 | 360 |

Parameters:

361 |
    362 |
  • ttl 363 | cache time-to-live in seconds (can be updated in following calls) 364 |
  • 365 |
366 | 367 |

Returns:

368 |
    369 | 370 | reverse and list tables, same as parseHosts. 371 |
372 | 373 | 374 |

See also:

375 | 378 | 379 | 380 |
381 |
382 | 383 | getResolv (ttl) 384 |
385 |
386 | returns the applyEnv results, but cached. 387 | Once ttl has been provided, only after it expires it will be parsed again.

388 | 389 |

NOTE: if cached, the SAME table will be returned, so do not modify them 390 | unless you know what you are doing! 391 | 392 | 393 |

Parameters:

394 |
    395 |
  • ttl 396 | cache time-to-live in seconds (can be updated in following calls) 397 |
  • 398 |
399 | 400 |

Returns:

401 |
    402 | 403 | configuration table, same as parseResolveConf. 404 |
405 | 406 | 407 |

See also:

408 | 411 | 412 | 413 |
414 |
415 |

Miscellaneous

416 | 417 |
418 |
419 | 420 | hostnameType (name) 421 |
422 |
423 | checks the hostname type; ipv4, ipv6, or name. 424 | Type is determined by exclusion, not by validation. So if it returns 'ipv6' then 425 | it can only be an ipv6, but it is not necessarily a valid ipv6 address. 426 | 427 | 428 |

Parameters:

429 |
    430 |
  • name 431 | the string to check (this may contain a port number) 432 |
  • 433 |
434 | 435 |

Returns:

436 |
    437 | 438 | string either; 'ipv4', 'ipv6', or 'name' 439 |
440 | 441 | 442 | 443 |

Usage:

444 |
    445 |
    hostnameType("123.123.123.123")  -->  "ipv4"
    446 | hostnameType("127.0.0.1:8080")   -->  "ipv4"
    447 | hostnameType("::1")              -->  "ipv6"
    448 | hostnameType("[::1]:8000")       -->  "ipv6"
    449 | hostnameType("some::thing")      -->  "ipv6", but invalid...
    450 |
451 | 452 |
453 |
454 | 455 | parseHostname (name) 456 |
457 |
458 | parses a hostname with an optional port. 459 | Does not validate the name/ip. IPv6 addresses are always returned in 460 | square brackets, even if the input wasn't. 461 | 462 | 463 |

Parameters:

464 |
    465 |
  • name 466 | the string to check (this may contain a port number) 467 |
  • 468 |
469 | 470 |

Returns:

471 |
    472 | 473 | name/ip + port (or nil) + type (one of: "ipv4", "ipv6", or "name") 474 |
475 | 476 | 477 | 478 | 479 |
480 |
481 | 482 | 483 |
484 |
485 |
486 | generated by LDoc 1.4.6 487 | Last updated 2021-07-06 11:55:24 488 |
489 |
490 | 491 | 492 | -------------------------------------------------------------------------------- /examples/client.lua: -------------------------------------------------------------------------------- 1 | local pretty = require("pl.pretty").write 2 | local client = require("resty.dns.client") 3 | client.init() 4 | 5 | local function go(host, typ) 6 | local resp, err 7 | if typ then 8 | resp, err = client.resolve(host, {qtype = client["TYPE_"..typ]}) 9 | else 10 | resp, err = client.resolve(host) 11 | end 12 | 13 | if not resp then 14 | print("Query failed: "..tostring(err)) 15 | end 16 | 17 | print(pretty(resp)) 18 | return resp 19 | end 20 | 21 | print "A TXT record" 22 | go ("txttest.thijsschreijer.nl", "TXT") 23 | 24 | print "Multiple A records" 25 | go "atest.thijsschreijer.nl" 26 | 27 | print "AAAA record" 28 | go ("google.com", "AAAA") 29 | 30 | print "A record redirected through 2 CNAME records" 31 | go "smtp.thijsschreijer.nl" 32 | 33 | print "Multiple SRV records" 34 | local resp = go "srvtest.thijsschreijer.nl" 35 | print "Priorities for this SRV record;" 36 | -- results will be sorted by priority 37 | print "> PRIMARY SET:" 38 | local last = resp[1].priority 39 | local backup = 0 40 | for i, rec in ipairs(resp) do 41 | if last ~= rec.priority then 42 | backup = backup + 1 43 | print("> BACKUP SET: "..backup) 44 | end 45 | print(" "..rec.priority, rec.target) 46 | end 47 | 48 | print "CNAME to multiple SRV records" 49 | go "cname2srv.thijsschreijer.nl" 50 | 51 | print "Non-matching type records (returns empty list)" 52 | go ("srvtest.thijsschreijer.nl", "A") --> not an A but an SRV type 53 | 54 | print "Non-existing records (returns server error, in a table)" 55 | go "IsNotHere.thijsschreijer.nl" 56 | 57 | print "From the /etc/hosts file; localhost" 58 | go "localhost" 59 | 60 | print "From the /etc/hosts file; localhost AAAA" 61 | go ("localhost", "AAAA") 62 | 63 | print "an IPv4 address" 64 | go ("1.2.3.4") 65 | 66 | print "an IPv6 address" 67 | go ("::1") 68 | 69 | print "an IPv4 address, as SRV" 70 | go ("1.2.3.4", "SRV") 71 | 72 | print "an IPv6 address, as SRV" 73 | go ("::1", "SRV") 74 | 75 | -------------------------------------------------------------------------------- /examples/utils.lua: -------------------------------------------------------------------------------- 1 | local dnsutils = require "dns.utils" 2 | local pretty = require("pl.pretty").write 3 | 4 | print("resolv.conf file;") 5 | print(pretty(dnsutils.parseResolvConf())) 6 | 7 | print("\nresolv.conf environment settings;") 8 | print(pretty(dnsutils.applyEnv({}))) 9 | 10 | print("\nresolv.conf including environment settings;") 11 | print(pretty(dnsutils.applyEnv(dnsutils.parseResolvConf()))) 12 | 13 | local rev, all = dnsutils.parseHosts() 14 | print("\nHosts file (all entries);") 15 | print(pretty(all)) 16 | print("\nHosts file (reverse lookup);") 17 | print(pretty(rev)) 18 | -------------------------------------------------------------------------------- /extra/README.md: -------------------------------------------------------------------------------- 1 | Log analyzer 2 | ============ 3 | 4 | A tool to help analyze dns client loggings, by pretty printing the logged data. 5 | 6 | This script will do 2 things: 7 | 8 | 1. split the log-file in separate files per worker process. Since dns data is not 9 | shared between workers it should be analyzed per worker. 10 | 2. pretty print the JSON snippets in the logs. It will expand the JSON into 11 | multiple lines, whilst retaining the log prefix with date and time stamps etc. 12 | 13 | It will handle normal logging, and the very verbose logging (when the extra log 14 | lines have been activated in the source code) 15 | 16 | Usage 17 | ===== 18 | 19 | The script should be called with 1 parameter; the log file to analyze. When ran, 20 | it will output several files, next to the original log file, each with the 21 | corresponding PID appended. 22 | 23 | Installation 24 | ============ 25 | 26 | It is a Lua script, and it requires Penlight and Cjson modules (can be installed 27 | through LuaRocks). 28 | 29 | You can run it from the main repo as; 30 | 31 | ``` 32 | $ luajit ./extra/clientlog.lua path/to/file/exported_logs.log 33 | ``` 34 | -------------------------------------------------------------------------------- /extra/clientlog.lua: -------------------------------------------------------------------------------- 1 | local indent = require("pl.text").indent 2 | local decode = require("cjson.safe").decode 3 | local readlines = require("pl.utils").readlines 4 | local writefile = require("pl.utils").writefile 5 | local pretty = require("pl.pretty").write 6 | local split = require("pl.utils").split 7 | 8 | 9 | local filename = arg[1] 10 | if not filename then 11 | print("1st argument (filename required) is missing") 12 | os.exit(1) 13 | end 14 | 15 | local no_pid = -1 16 | local lines = readlines(filename) 17 | local files = setmetatable({}, { 18 | __index = function(self, pid) 19 | local new_file = {} 20 | print("found pid: ", pid) 21 | rawset(self, pid, new_file) 22 | if pid ~= no_pid then 23 | -- if there is a preface, copy it into the new file 24 | for i, v in ipairs(self[no_pid]) do 25 | new_file[i] = v 26 | end 27 | end 28 | return new_file 29 | end, 30 | }) 31 | 32 | -- grab pid: 33 | local PID_PATTERN = "%[%l+%] (%d+)#%d+:" -- returns only the pid 34 | -- split line: 35 | local SPLIT_PATTERN = "^(.-:%d*: )(.+)$" -- returns prefix + "[dns-client] ..." 36 | -- grab json: 37 | local JSON_PATTERN = "^(.-)({.+})(.*)$" -- returns prefix, JSON, postfix 38 | -- grab "Tried" json: 39 | local TRIED_PATTERN = "^(.-Tried: )(%[.+%])(.*)$" -- returns prefix, JSON, postfix 40 | 41 | 42 | for i = 1, #lines do 43 | local line = lines[i] --("%6.0f "):format(i) .. lines[i] 44 | local pid = line:match(PID_PATTERN) 45 | if not pid then 46 | -- no PID found, inject in 'no_pid' table 47 | table.insert(files[no_pid], line) 48 | else 49 | local line_prefix, message = line:match(SPLIT_PATTERN) 50 | if not line_prefix then 51 | -- not a dns-client line, just insert 52 | table.insert(files[pid], line) 53 | else 54 | local pre_json, json, post_json = message:match(TRIED_PATTERN) 55 | if not pre_json then 56 | pre_json, json, post_json = message:match(JSON_PATTERN) 57 | end 58 | if not pre_json then 59 | -- no json to expand, just insert 60 | table.insert(files[pid], line) 61 | else 62 | -- json to expand 63 | local json_table = decode(json) 64 | if not json_table then 65 | json_table = decode("["..json.."]") 66 | end 67 | if not json_table then 68 | -- failed decoding json 69 | table.insert(files[pid], line) 70 | else 71 | -- pretty print json 72 | local pretty_json = indent(pretty(json_table), 4) 73 | if pretty_json:sub(-1,-1) == "\n" then pretty_json = pretty_json:sub(1,-2) end 74 | local lines = split(pretty_json, "\n", true) 75 | local file = files[pid] 76 | 77 | table.insert(file, line_prefix .. pre_json) 78 | for _, entry in ipairs(lines) do 79 | table.insert(file, line_prefix .. entry) 80 | end 81 | if #post_json > 0 then 82 | table.insert(file, line_prefix .. " " .. post_json) 83 | end 84 | end 85 | end 86 | end 87 | end 88 | end 89 | 90 | for pid, file in pairs(files) do 91 | if pid == no_pid then pid = "unknown" end 92 | local name = filename .. "_pid-" .. pid .. ".log" 93 | print("writing: ", name) 94 | writefile(name, table.concat(file, "\n")) 95 | end 96 | 97 | print("\n") 98 | -------------------------------------------------------------------------------- /lua-resty-dns-client-6.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | local package_name = "lua-resty-dns-client" 2 | local package_version = "6.0.2" 3 | local rockspec_revision = "1" 4 | local github_account_name = "Kong" 5 | local github_repo_name = package_name 6 | 7 | 8 | package = package_name 9 | version = package_version.."-"..rockspec_revision 10 | source = { 11 | url = "git://github.com/"..github_account_name.."/"..github_repo_name..".git", 12 | tag = package_version, 13 | } 14 | description = { 15 | summary = "DNS library", 16 | detailed = [[ 17 | DNS client library. Including utilities to parse configuration files and 18 | a load balancers for round-robin, consistent-hashing, and least- 19 | connections approaches. 20 | ]], 21 | homepage = "https://github.com/"..github_account_name.."/"..github_repo_name, 22 | license = "Apache 2.0" 23 | } 24 | dependencies = { 25 | "lua >= 5.1, < 5.4", 26 | "penlight ~> 1", 27 | "lrandom", 28 | "lua-resty-timer ~> 1", 29 | "binaryheap >= 0.4", 30 | "luaxxhash >= 1.0", 31 | } 32 | build = { 33 | type = "builtin", 34 | modules = { 35 | ["resty.dns.utils"] = "src/resty/dns/utils.lua", 36 | ["resty.dns.client"] = "src/resty/dns/client.lua", 37 | ["resty.dns.balancer.base"] = "src/resty/dns/balancer/base.lua", 38 | ["resty.dns.balancer.consistent_hashing"] = "src/resty/dns/balancer/consistent_hashing.lua", 39 | ["resty.dns.balancer.least_connections"] = "src/resty/dns/balancer/least_connections.lua", 40 | ["resty.dns.balancer.handle"] = "src/resty/dns/balancer/handle.lua", 41 | ["resty.dns.balancer.round_robin"] = "src/resty/dns/balancer/round_robin.lua", 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /rbusted: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env resty 2 | 3 | local DEFAULT_RESTY_FLAGS="-c 4096" 4 | 5 | if not os.getenv("BUSTED_RESPAWNED") then 6 | -- initial run, so go update the environment 7 | -- rebuild the invoked commandline, while inserting extra resty-flags 8 | local resty_flags = DEFAULT_RESTY_FLAGS 9 | local cmd = { "exec" } 10 | for i = -1, #arg do 11 | if arg[i]:sub(1, 12) == "RESTY_FLAGS=" then 12 | resty_flags = arg[i]:sub(13, -1) 13 | 14 | else 15 | table.insert(cmd, "'" .. arg[i] .. "'") 16 | end 17 | end 18 | 19 | if resty_flags then 20 | table.insert(cmd, 3, resty_flags) 21 | end 22 | 23 | local _, _, rc = os.execute("export BUSTED_RESPAWNED=1; " .. table.concat(cmd, " ")) 24 | os.exit(rc) 25 | end 26 | 27 | 28 | -- remove openresty write guard on _G 29 | setmetatable(_G, nil) 30 | 31 | package.path = "?/init.lua;"..package.path 32 | 33 | if ngx ~= nil then 34 | ngx.exit = function() end 35 | end 36 | 37 | -- disable globals warning 38 | setmetatable(_G, nil) 39 | 40 | -- Busted command-line runner 41 | require 'busted.runner'({ standalone = false }) 42 | -------------------------------------------------------------------------------- /spec/balancer/base_spec.lua: -------------------------------------------------------------------------------- 1 | 2 | local client, balancer_base 3 | 4 | 5 | local helpers = require "spec.test_helpers" 6 | --local gettime = helpers.gettime 7 | --local sleep = helpers.sleep 8 | local dnsSRV = function(...) return helpers.dnsSRV(client, ...) end 9 | local dnsA = function(...) return helpers.dnsA(client, ...) end 10 | --local dnsAAAA = function(...) return helpers.dnsAAAA(client, ...) end 11 | local dnsExpire = helpers.dnsExpire 12 | 13 | 14 | describe("[balancer_base]", function() 15 | 16 | local snapshot 17 | 18 | setup(function() 19 | _G.package.loaded["resty.dns.client"] = nil -- make sure module is reloaded 20 | balancer_base = require "resty.dns.balancer.base" 21 | client = require "resty.dns.client" 22 | end) 23 | 24 | 25 | before_each(function() 26 | assert(client.init { 27 | hosts = {}, 28 | resolvConf = { 29 | "nameserver 8.8.8.8" 30 | }, 31 | }) 32 | snapshot = assert:snapshot() 33 | end) 34 | 35 | 36 | after_each(function() 37 | snapshot:revert() -- undo any spying/stubbing etc. 38 | collectgarbage() 39 | collectgarbage() 40 | end) 41 | 42 | 43 | 44 | describe("handles", function() 45 | 46 | local b, gc_count, release, release_ignore 47 | 48 | setup(function() 49 | b = balancer_base.new({ 50 | dns = client, 51 | }) 52 | 53 | function b:newAddress(addr) 54 | addr = self.super.newAddress(self, addr) 55 | function addr:release(handle, ignore) 56 | if ignore then 57 | release_ignore = (release_ignore or 0) + 1 58 | else 59 | release = (release or 0) + 1 60 | end 61 | end 62 | end 63 | 64 | local gc = function(handle) 65 | gc_count = (gc_count or 0) + 1 66 | end 67 | 68 | function b:getPeer(cacheOnly, handle, hashValue) 69 | handle = handle or self:getHandle(gc) 70 | local addr = self.addresses[1] 71 | handle.address = addr 72 | return addr.ip, addr.port, addr.host.hostname, handle 73 | end 74 | end) 75 | 76 | before_each(function() 77 | dnsSRV({ 78 | { name = "konghq.com", target = "1.1.1.1", port = 3, weight = 6 }, 79 | }) 80 | gc_count = 0 81 | release = 0 82 | release_ignore = 0 83 | end) 84 | 85 | it("releasing a handle doesn't call GC", function() 86 | b:addHost("konghq.com", 8000, 100) 87 | local _, _, _, handle = b:getPeer() 88 | handle:release(false) 89 | collectgarbage() 90 | collectgarbage() 91 | assert.equal(0, gc_count) 92 | assert.equal(1, release) 93 | assert.equal(0, release_ignore) 94 | end) 95 | 96 | it("releasing a handle doesn't call GC (ignore)", function() 97 | b:addHost("konghq.com", 8000, 100) 98 | local _, _, _, handle = b:getPeer() 99 | handle:release(true) 100 | collectgarbage() 101 | collectgarbage() 102 | assert.equal(0, gc_count) 103 | assert.equal(0, release) 104 | assert.equal(1, release_ignore) 105 | end) 106 | 107 | it("not-releasing a handle does call GC, with ignore", function() 108 | b:addHost("konghq.com", 8000, 100) 109 | local _, _, _, handle = b:getPeer() --luacheck: ignore 110 | handle = nil 111 | collectgarbage() 112 | collectgarbage() 113 | assert.equal(1, gc_count) 114 | assert.equal(0, release) 115 | assert.equal(0, release_ignore) 116 | end) 117 | 118 | it("releasing re-uses a handle", function() 119 | b:addHost("konghq.com", 8000, 100) 120 | local _, _, _, handle = b:getPeer() 121 | local handle_id = tostring(handle) 122 | handle:release(false) 123 | handle = nil --luacheck: ignore 124 | collectgarbage() 125 | collectgarbage() 126 | _, _, _, handle = b:getPeer() 127 | assert.equal(handle_id, tostring(handle)) 128 | end) 129 | 130 | it("not-releasing a handle does not re-use it", function() 131 | b:addHost("konghq.com", 8000, 100) 132 | local _, _, _, handle = b:getPeer() 133 | local handle_id = tostring(handle) 134 | handle = nil --luacheck: ignore 135 | collectgarbage() 136 | collectgarbage() 137 | _, _, _, handle = b:getPeer() 138 | --assert.not_equal(handle_id, tostring(handle)) 139 | if handle_id == tostring(handle) then 140 | -- hmmmmm they are the same.... 141 | -- seems that occasionally the new table gets allocated at the exact 142 | -- same location, causing false positives. So let's drop the new table 143 | -- again, and check that the GC was called twice! 144 | handle = nil --luacheck: ignore 145 | collectgarbage() 146 | collectgarbage() 147 | assert.equal(2, gc_count) 148 | assert.equal(0, release) 149 | assert.equal(0, release_ignore) 150 | end 151 | 152 | end) 153 | 154 | end) 155 | 156 | 157 | describe("callbacks", function() 158 | 159 | local list 160 | local handler = function(balancer, eventname, address, ip, port, hostname, hostheader) 161 | assert(({ 162 | added = true, 163 | removed = true, 164 | health = true, 165 | })[eventname], "Unknown eventname: " .. tostring(eventname)) 166 | 167 | if eventname == "added" then 168 | -- the 'host' property has been cleared by the time the event executes 169 | assert(balancer == address.host.balancer) 170 | assert.is.equal(address.host.hostname, hostname) 171 | end 172 | if eventname == "added" or eventname == "removed" then 173 | assert.is.equal(address.ip, ip) 174 | assert.is.equal(address.port, port) 175 | end 176 | list[#list + 1] = { 177 | balancer, eventname, address, ip, port, hostname, hostheader, 178 | } 179 | end 180 | 181 | before_each(function() ngx.sleep(0) end) 182 | after_each(function() ngx.sleep(0) end) 183 | 184 | 185 | it("on adding", function() 186 | local b = balancer_base.new({ 187 | dns = client, 188 | callback = handler, 189 | }) 190 | 191 | list = {} 192 | b:addHost("localhost", 80) 193 | ngx.sleep(0.1) 194 | 195 | assert.equal(2, #list) 196 | assert.equal(b, list[1][1]) 197 | assert.equal("health", list[1][2]) 198 | assert.equal(true, list[1][3]) 199 | 200 | assert.equal(b, list[2][1]) 201 | assert.equal("added", list[2][2]) 202 | assert.is.table(list[2][3]) 203 | assert.equal("127.0.0.1", list[2][4]) 204 | assert.equal(80, list[2][5]) 205 | assert.equal("localhost", list[2][6]) -- hostname 206 | assert.equal("localhost", list[2][7]) -- hostheader 207 | end) 208 | 209 | 210 | it("on removing", function() 211 | local b = balancer_base.new({ 212 | dns = client, 213 | callback = handler, 214 | }) 215 | list = {} 216 | b:addHost("localhost", 80) 217 | ngx.sleep(0.1) 218 | 219 | assert.equal(2, #list) 220 | assert.equal(b, list[1][1]) 221 | assert.equal("health", list[1][2]) 222 | assert.equal(true, list[1][3]) 223 | 224 | assert.equal(b, list[2][1]) 225 | assert.equal("added", list[2][2]) 226 | assert.is.table(list[2][3]) 227 | assert.equal("127.0.0.1", list[2][4]) 228 | assert.equal(80, list[2][5]) 229 | assert.equal("localhost", list[2][6]) -- hostname 230 | assert.equal("localhost", list[2][7]) -- hostheader 231 | 232 | b:removeHost("localhost", 80) 233 | ngx.sleep(0.1) 234 | 235 | assert.equal(4, #list) 236 | assert.equal(b, list[3][1]) 237 | assert.equal("health", list[3][2]) 238 | assert.equal(false, list[3][3]) 239 | 240 | assert.equal(b, list[4][1]) 241 | assert.equal("removed", list[4][2]) 242 | assert.equal(list[2][3], list[4][3]) -- same address object as added 243 | assert.equal("127.0.0.1", list[4][4]) 244 | assert.equal(80, list[4][5]) 245 | assert.equal("localhost", list[4][6]) -- hostname 246 | assert.equal("localhost", list[4][7]) -- hostheader 247 | end) 248 | 249 | end) 250 | 251 | 252 | 253 | describe("event order", function() 254 | 255 | local event_list, b 256 | 257 | setup(function() 258 | b = balancer_base.new({ 259 | dns = client, 260 | }) 261 | local addrInfo = function(addr, event) 262 | return { 263 | _event = event, 264 | address = addr.ip..":"..addr.port, 265 | weight = addr.weight, 266 | disabled = addr.disabled, 267 | available = addr.available, 268 | } 269 | end 270 | local hostInfo = function(host, event) 271 | local info = { 272 | _event = event, 273 | host = host.hostname..":"..host.port, 274 | nodeWeight = host.nodeWeight, 275 | } 276 | for i, addr in ipairs(host.addresses) do 277 | info[i] = addrInfo(addr) 278 | end 279 | return info 280 | end 281 | 282 | function b:onAddAddress(addr) 283 | table.insert(event_list, addrInfo(addr, "onAddAddress")) 284 | self.super.onAddAddress(self, addr) 285 | end 286 | function b:onRemoveAddress(addr) 287 | table.insert(event_list, addrInfo(addr, "onRemoveAddress")) 288 | self.super.onRemoveAddress(self, addr) 289 | end 290 | function b:afterHostUpdate(host) 291 | table.insert(event_list, hostInfo(host, "afterHostUpdate")) 292 | self.super.afterHostUpdate(self, host) 293 | end 294 | function b:beforeHostDelete(host) 295 | table.insert(event_list, hostInfo(host, "beforeHostDelete")) 296 | self.super.beforeHostDelete(self, host) 297 | end 298 | function b:touch_all() 299 | for _, addr in ipairs(self.addresses) do 300 | addr:getPeer() -- will force dns update of expired records 301 | end 302 | end 303 | end) 304 | 305 | 306 | local record 307 | before_each(function() 308 | event_list = {} 309 | record = dnsSRV({ 310 | { name = "konghq.com", target = "1.1.1.1", port = 3, weight = 6 }, 311 | { name = "konghq.com", target = "2.2.2.2", port = 5, weight = 7 }, 312 | }) 313 | end) 314 | 315 | 316 | after_each(function() 317 | -- clear all the hosts from the test balancer 318 | for _, host in ipairs(b.hosts) do 319 | b:removeHost(host.hostname, host.port) 320 | end 321 | end) 322 | 323 | 324 | 325 | it("when adding a host", function() 326 | b:addHost("konghq.com", 8000, 100) 327 | assert.same({ 328 | { 329 | _event = 'onAddAddress', 330 | address = '1.1.1.1:3', 331 | available = true, 332 | disabled = false, 333 | weight = 6 334 | }, { 335 | _event = 'onAddAddress', 336 | address = '2.2.2.2:5', 337 | available = true, 338 | disabled = false, 339 | weight = 7, 340 | }, { 341 | _event = 'afterHostUpdate', 342 | host = 'konghq.com:8000', 343 | nodeWeight = 100, 344 | { 345 | address = '1.1.1.1:3', 346 | available = true, 347 | disabled = false, 348 | weight = 6 349 | }, { 350 | address = '2.2.2.2:5', 351 | available = true, 352 | disabled = false, 353 | weight = 7, 354 | }, 355 | } 356 | }, event_list) 357 | end) 358 | 359 | 360 | it("when removing a host", function() 361 | b:addHost("konghq.com", 8000, 100) 362 | event_list = {} -- clear the list so we only get relevant events 363 | b:removeHost("konghq.com", 8000) 364 | assert.same({ 365 | { 366 | _event = 'beforeHostDelete', 367 | host = 'konghq.com:8000', 368 | nodeWeight = 100, 369 | { -- both addresses still here, but disabled! 370 | address = '1.1.1.1:3', 371 | available = true, 372 | disabled = true, -- marked as disabled! 373 | weight = 0, -- weight reduced to 0! 374 | }, { 375 | address = '2.2.2.2:5', 376 | available = true, 377 | disabled = true, -- marked as disabled! 378 | weight = 0, -- weight reduced to 0! 379 | }, 380 | }, { 381 | _event = 'onRemoveAddress', 382 | address = '2.2.2.2:5', 383 | available = true, 384 | disabled = true, -- marked as disabled! 385 | weight = 0, -- weight reduced to 0! 386 | }, { 387 | _event = 'onRemoveAddress', 388 | address = '1.1.1.1:3', 389 | available = true, 390 | disabled = true, -- marked as disabled! 391 | weight = 0, -- weight reduced to 0! 392 | }, 393 | }, event_list) 394 | end) 395 | 396 | 397 | it("when removing a DNS record entry", function() 398 | b:addHost("konghq.com", 8000, 100) 399 | dnsExpire(record) -- expire initial record 400 | record = dnsSRV({ -- insert a new record, 1 entry removed 401 | { name = "konghq.com", target = "1.1.1.1", port = 3, weight = 6 }, 402 | }) 403 | event_list = {} -- clear the list so we only get relevant events 404 | b:touch_all() -- touch them and force dns updates 405 | assert.same({ 406 | { 407 | _event = 'afterHostUpdate', 408 | host = 'konghq.com:8000', 409 | nodeWeight = 100, 410 | { 411 | address = '1.1.1.1:3', 412 | available = true, 413 | disabled = false, 414 | weight = 6, 415 | }, { 416 | address = '2.2.2.2:5', 417 | available = true, 418 | disabled = true, -- marked as disabled! 419 | weight = 0, -- weight reduced to 0! 420 | }, 421 | }, { 422 | _event = 'onRemoveAddress', 423 | address = '2.2.2.2:5', 424 | available = true, 425 | disabled = true, -- marked as disabled! 426 | weight = 0, -- weight reduced to 0! 427 | } , 428 | }, event_list) 429 | end) 430 | 431 | 432 | it("when adding a DNS record entry", function() 433 | b:addHost("konghq.com", 8000, 100) 434 | dnsExpire(record) -- expire initial record 435 | record = dnsSRV({ -- insert a new record, 1 new weight 436 | { name = "konghq.com", target = "1.1.1.1", port = 3, weight = 6 }, 437 | { name = "konghq.com", target = "2.2.2.2", port = 5, weight = 7 }, 438 | { name = "konghq.com", target = "8.8.8.8", port = 9, weight = 10 }, 439 | }) 440 | event_list = {} -- clear the list so we only get relevant events 441 | b:touch_all() -- touch them and force dns updates 442 | assert.same({ 443 | { 444 | _event = 'onAddAddress', 445 | address = '8.8.8.8:9', 446 | available = true, 447 | disabled = false, 448 | weight = 10, 449 | }, { 450 | _event = 'afterHostUpdate', 451 | host = 'konghq.com:8000', 452 | nodeWeight = 100, 453 | { 454 | address = '1.1.1.1:3', 455 | available = true, 456 | disabled = false, 457 | weight = 6, 458 | }, { 459 | address = '2.2.2.2:5', 460 | available = true, 461 | disabled = false, 462 | weight = 7, 463 | }, { 464 | address = '8.8.8.8:9', 465 | available = true, 466 | disabled = false, 467 | weight = 10, 468 | }, 469 | }, 470 | }, event_list) 471 | end) 472 | 473 | 474 | it("when changing an SRV weight", function() 475 | b:addHost("konghq.com", 8000, 100) 476 | dnsExpire(record) -- expire initial record 477 | record = dnsSRV({ -- insert a new record, 1 new weight 478 | { name = "konghq.com", target = "1.1.1.1", port = 3, weight = 6 }, 479 | { name = "konghq.com", target = "2.2.2.2", port = 5, weight = 50 }, 480 | }) 481 | event_list = {} -- clear the list so we only get relevant events 482 | b:touch_all() -- touch them and force dns updates 483 | assert.same({ 484 | { 485 | _event = 'afterHostUpdate', 486 | host = 'konghq.com:8000', 487 | nodeWeight = 100, 488 | { 489 | address = '1.1.1.1:3', 490 | available = true, 491 | disabled = false, 492 | weight = 6, 493 | }, { 494 | address = '2.2.2.2:5', 495 | available = true, 496 | disabled = false, 497 | weight = 50, -- Updated weight! 498 | }, 499 | }, 500 | }, event_list) 501 | end) 502 | 503 | 504 | it("when changing an non-SRV weight", function() 505 | record = dnsA({ 506 | { name = "getkong.org", address = "1.2.3.4" }, 507 | { name = "getkong.org", address = "5.6.7.8" }, 508 | }) 509 | b:addHost("getkong.org", 8000, 100) 510 | event_list = {} -- clear the list so we only get relevant events 511 | b:addHost("getkong.org", 8000, 5) -- change the weights 512 | assert.same({ 513 | { 514 | _event = 'afterHostUpdate', 515 | host = 'getkong.org:8000', 516 | nodeWeight = 5, 517 | { 518 | address = '1.2.3.4:8000', 519 | available = true, 520 | disabled = false, 521 | weight = 5, -- weight updated to 5! 522 | }, { 523 | address = '5.6.7.8:8000', 524 | available = true, 525 | disabled = false, 526 | weight = 5, -- weight updated to 5! 527 | }, 528 | }, 529 | }, event_list) 530 | end) 531 | 532 | end) 533 | 534 | end) 535 | -------------------------------------------------------------------------------- /spec/balancer/handle_spec.lua: -------------------------------------------------------------------------------- 1 | local spy = require "luassert.spy" 2 | 3 | 4 | describe("[handle]", function() 5 | 6 | 7 | local handle 8 | 9 | before_each(function() 10 | handle = require "resty.dns.balancer.handle" 11 | end) 12 | 13 | 14 | 15 | it("returning doesn't trigger __gc", function() 16 | local s = spy.new(function() end) 17 | local h = handle.get(s) 18 | handle.release(h) 19 | h = nil --luacheck: ignore 20 | collectgarbage() 21 | collectgarbage() 22 | assert.spy(s).was_not.called() 23 | end) 24 | 25 | 26 | it("not returning triggers __gc", function() 27 | local s = spy.new(function() end) 28 | local h = handle.get(s) --luacheck: ignore 29 | h = nil 30 | collectgarbage() 31 | collectgarbage() 32 | assert.spy(s).was.called() 33 | end) 34 | 35 | 36 | it("not returning doesn't fail without __gc", function() 37 | local h = handle.get() --luacheck: ignore 38 | h = nil 39 | collectgarbage() 40 | collectgarbage() 41 | end) 42 | 43 | 44 | it("handles get re-used", function() 45 | local h = handle.get() 46 | local id = tostring(h) 47 | handle.release(h) 48 | h = handle.get() 49 | assert.equal(id, tostring(h)) 50 | end) 51 | 52 | 53 | it("handles get cleared before re-use", function() 54 | local h = handle.get() 55 | local id = tostring(h) 56 | h.hello = "world" 57 | handle.release(h) 58 | h = handle.get() 59 | 60 | assert.equal(id, tostring(h)) 61 | assert.is_nil(h.hello) 62 | end) 63 | 64 | 65 | it("beyond cache-size, handles are dropped", function() 66 | handle.setCacheSize(1) 67 | local h1 = handle.get() 68 | local h2 = handle.get() 69 | local id1 = tostring(h1) 70 | local id2 = tostring(h2) 71 | handle.release(h1) 72 | handle.release(h2) 73 | h1 = handle.get() 74 | h2 = handle.get() 75 | assert.equal(id1, tostring(h1)) 76 | assert.not_equal(id2, tostring(h2)) 77 | end) 78 | 79 | 80 | it("__gc is not invoked when handle beyond cache size is dropped", function() 81 | handle.setCacheSize(1) 82 | local s = spy.new(function() end) 83 | local h1 = handle.get(s) 84 | local h2 = handle.get(s) 85 | handle.release(h1) -- returned to cache 86 | handle.release(h2) -- dropped 87 | h1 = nil --luacheck: ignore 88 | h2 = nil --luacheck: ignore 89 | collectgarbage() 90 | collectgarbage() 91 | assert.spy(s).was_not.called() 92 | end) 93 | 94 | 95 | it("reducing cache-size drops whatever is too many", function() 96 | handle.setCacheSize(2) 97 | local h1 = handle.get() 98 | local h2 = handle.get() 99 | local id1 = tostring(h1) 100 | local id2 = tostring(h2) 101 | handle.release(h1) -- returned to cache 102 | handle.release(h2) -- returned to cache 103 | handle.setCacheSize(1) -- the last one is now dropped 104 | h1 = handle.get() 105 | h2 = handle.get() 106 | assert.equal(id1, tostring(h1)) 107 | assert.not_equal(id2, tostring(h2)) 108 | end) 109 | 110 | end) 111 | -------------------------------------------------------------------------------- /spec/balancer/least_connections_spec.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | local client, lcb 4 | 5 | local helpers = require "spec.test_helpers" 6 | --local gettime = helpers.gettime 7 | --local sleep = helpers.sleep 8 | local dnsSRV = function(...) return helpers.dnsSRV(client, ...) end 9 | local dnsA = function(...) return helpers.dnsA(client, ...) end 10 | --local dnsAAAA = function(...) return helpers.dnsAAAA(client, ...) end 11 | --local dnsExpire = helpers.dnsExpire 12 | local t_insert = table.insert 13 | 14 | 15 | local validate_lcb 16 | do 17 | -- format string to fixed length for table-like display 18 | local function size(str, l) 19 | local isnum = type(str) == "number" 20 | str = tostring(str) 21 | str = str:sub(1, l) 22 | if isnum then 23 | str = string.rep(" ", l - #str) .. str 24 | else 25 | str = str .. string.rep(" ", l - #str) 26 | end 27 | return str 28 | end 29 | 30 | function validate_lcb(b, debug) 31 | local available, unavailable = 0, 0 32 | if debug then 33 | print("host.hostname addr.ip weight count sort-order") 34 | end 35 | for i, addr in ipairs(b.addresses) do 36 | local display = {} 37 | t_insert(display, size(addr.host.hostname, 15)) 38 | t_insert(display, size(addr.ip, 15)) 39 | t_insert(display, size(addr.weight, 5)) 40 | t_insert(display, size(addr.connectionCount, 5)) 41 | if b.binaryHeap:valueByPayload(addr) then 42 | t_insert(display, size(("%.10f"):format(b.binaryHeap:valueByPayload(addr)), 14)) 43 | else 44 | t_insert(display, size(b.binaryHeap:valueByPayload(addr), 14)) 45 | end 46 | if b.binaryHeap:valueByPayload(addr) then 47 | -- it's in the heap 48 | assert(not addr.disabled, "should be enabled when in the heap") 49 | assert(addr.available, "should be available when in the heap") 50 | available = available + 1 51 | assert(b.binaryHeap:valueByPayload(addr) == (addr.connectionCount+1)/addr.weight) 52 | else 53 | assert(not addr.disabled, "should be enabled when not in the heap") 54 | assert(not addr.available, "should not be available when not in the heap") 55 | unavailable = unavailable + 1 56 | end 57 | if debug then 58 | print(table.concat(display, " ")) 59 | end 60 | end 61 | assert(available + unavailable == #b.addresses, "mismatch in counts") 62 | return b 63 | end 64 | end 65 | 66 | 67 | describe("[least-connections]", function() 68 | 69 | local snapshot 70 | 71 | setup(function() 72 | _G.package.loaded["resty.dns.client"] = nil -- make sure module is reloaded 73 | lcb = require "resty.dns.balancer.least_connections" 74 | client = require "resty.dns.client" 75 | end) 76 | 77 | 78 | before_each(function() 79 | assert(client.init { 80 | hosts = {}, 81 | resolvConf = { 82 | "nameserver 8.8.8.8" 83 | }, 84 | }) 85 | snapshot = assert:snapshot() 86 | end) 87 | 88 | 89 | after_each(function() 90 | snapshot:revert() -- undo any spying/stubbing etc. 91 | collectgarbage() 92 | collectgarbage() 93 | end) 94 | 95 | 96 | 97 | describe("new()", function() 98 | 99 | it("inserts provided hosts", function() 100 | dnsA({ 101 | { name = "konghq.com", address = "1.2.3.4" }, 102 | }) 103 | dnsA({ 104 | { name = "github.com", address = "1.2.3.4" }, 105 | }) 106 | dnsA({ 107 | { name = "getkong.org", address = "1.2.3.4" }, 108 | }) 109 | local b = validate_lcb(lcb.new({ 110 | dns = client, 111 | hosts = { 112 | "konghq.com", -- name only, as string 113 | { name = "github.com" }, -- name only, as table 114 | { name = "getkong.org", port = 80, weight = 25 }, -- fully specified, as table 115 | }, 116 | })) 117 | assert.equal("konghq.com", b.addresses[1].host.hostname) 118 | assert.equal("github.com", b.addresses[2].host.hostname) 119 | assert.equal("getkong.org", b.addresses[3].host.hostname) 120 | end) 121 | end) 122 | 123 | 124 | describe("getPeer()", function() 125 | 126 | it("honours weights", function() 127 | dnsSRV({ 128 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 129 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 130 | }) 131 | local b = validate_lcb(lcb.new({ 132 | dns = client, 133 | hosts = { "konghq.com" }, 134 | })) 135 | 136 | local counts = {} 137 | local handles = {} 138 | for i = 1,70 do 139 | local ip, _, _, handle = b:getPeer() 140 | counts[ip] = (counts[ip] or 0) + 1 141 | t_insert(handles, handle) -- don't let them get GC'ed 142 | end 143 | 144 | validate_lcb(b) 145 | 146 | assert.same({ 147 | ["20.20.20.20"] = 20, 148 | ["50.50.50.50"] = 50 149 | }, counts) 150 | end) 151 | 152 | 153 | it("first returns top weights, on a 0-connection balancer", function() 154 | dnsSRV({ 155 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 156 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 157 | }) 158 | local b = validate_lcb(lcb.new({ 159 | dns = client, 160 | hosts = { "konghq.com" }, 161 | })) 162 | 163 | local handles = {} 164 | local ip, _, handle 165 | 166 | -- first try 167 | ip, _, _, handle= b:getPeer() 168 | t_insert(handles, handle) -- don't let them get GC'ed 169 | validate_lcb(b) 170 | assert.equal("50.50.50.50", ip) 171 | 172 | -- second try 173 | ip, _, _, handle= b:getPeer() 174 | t_insert(handles, handle) -- don't let them get GC'ed 175 | validate_lcb(b) 176 | assert.equal("50.50.50.50", ip) 177 | 178 | -- third try 179 | ip, _, _, handle= b:getPeer() 180 | t_insert(handles, handle) -- don't let them get GC'ed 181 | validate_lcb(b) 182 | assert.equal("20.20.20.20", ip) 183 | end) 184 | 185 | 186 | it("doesn't use unavailable addresses", function() 187 | dnsSRV({ 188 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 189 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 190 | }) 191 | local b = validate_lcb(lcb.new({ 192 | dns = client, 193 | hosts = { "konghq.com" }, 194 | })) 195 | 196 | -- mark one as unavailable 197 | b:setAddressStatus(false, "50.50.50.50", 80, "konghq.com") 198 | local counts = {} 199 | local handles = {} 200 | for i = 1,70 do 201 | local ip, _, _, handle = b:getPeer() 202 | counts[ip] = (counts[ip] or 0) + 1 203 | t_insert(handles, handle) -- don't let them get GC'ed 204 | end 205 | 206 | validate_lcb(b) 207 | 208 | assert.same({ 209 | ["20.20.20.20"] = 70, 210 | ["50.50.50.50"] = nil, 211 | }, counts) 212 | end) 213 | 214 | 215 | it("uses reenabled (available) addresses again", function() 216 | dnsSRV({ 217 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 218 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 219 | }) 220 | local b = validate_lcb(lcb.new({ 221 | dns = client, 222 | hosts = { "konghq.com" }, 223 | })) 224 | 225 | -- mark one as unavailable 226 | b:setAddressStatus(false, "20.20.20.20", 80, "konghq.com") 227 | local counts = {} 228 | local handles = {} 229 | for i = 1,70 do 230 | local ip, _, _, handle = b:getPeer() 231 | counts[ip] = (counts[ip] or 0) + 1 232 | t_insert(handles, handle) -- don't let them get GC'ed 233 | end 234 | 235 | validate_lcb(b) 236 | 237 | assert.same({ 238 | ["20.20.20.20"] = nil, 239 | ["50.50.50.50"] = 70, 240 | }, counts) 241 | 242 | -- let's do another 70, after resetting 243 | b:setAddressStatus(true, "20.20.20.20", 80, "konghq.com") 244 | for i = 1,70 do 245 | local ip, _, _, handle = b:getPeer() 246 | counts[ip] = (counts[ip] or 0) + 1 247 | t_insert(handles, handle) -- don't let them get GC'ed 248 | end 249 | 250 | validate_lcb(b) 251 | 252 | assert.same({ 253 | ["20.20.20.20"] = 40, 254 | ["50.50.50.50"] = 100, 255 | }, counts) 256 | end) 257 | 258 | 259 | end) 260 | 261 | 262 | describe("retrying getPeer()", function() 263 | 264 | it("does not return already failed addresses", function() 265 | dnsSRV({ 266 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 267 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 268 | { name = "konghq.com", target = "70.70.70.70", port = 80, weight = 70 }, 269 | }) 270 | local b = validate_lcb(lcb.new({ 271 | dns = client, 272 | hosts = { "konghq.com" }, 273 | })) 274 | 275 | local tried = {} 276 | local ip, _, handle 277 | -- first try 278 | ip, _, _, handle = b:getPeer() 279 | tried[ip] = (tried[ip] or 0) + 1 280 | validate_lcb(b) 281 | 282 | 283 | -- 1st retry 284 | ip, _, _, handle = b:getPeer(nil, handle) 285 | assert.is_nil(tried[ip]) 286 | tried[ip] = (tried[ip] or 0) + 1 287 | validate_lcb(b) 288 | 289 | -- 2nd retry 290 | ip, _, _, _ = b:getPeer(nil, handle) 291 | assert.is_nil(tried[ip]) 292 | tried[ip] = (tried[ip] or 0) + 1 293 | validate_lcb(b) 294 | 295 | assert.same({ 296 | ["20.20.20.20"] = 1, 297 | ["50.50.50.50"] = 1, 298 | ["70.70.70.70"] = 1, 299 | }, tried) 300 | end) 301 | 302 | 303 | it("retries, after all adresses failed, restarts with previously failed ones", function() 304 | dnsSRV({ 305 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 306 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 307 | { name = "konghq.com", target = "70.70.70.70", port = 80, weight = 70 }, 308 | }) 309 | local b = validate_lcb(lcb.new({ 310 | dns = client, 311 | hosts = { "konghq.com" }, 312 | })) 313 | 314 | local tried = {} 315 | local ip, _, handle 316 | 317 | for i = 1,6 do 318 | ip, _, _, handle = b:getPeer(nil, handle) 319 | tried[ip] = (tried[ip] or 0) + 1 320 | validate_lcb(b) 321 | end 322 | 323 | assert.same({ 324 | ["20.20.20.20"] = 2, 325 | ["50.50.50.50"] = 2, 326 | ["70.70.70.70"] = 2, 327 | }, tried) 328 | end) 329 | 330 | 331 | it("releases the previous connection", function() 332 | dnsSRV({ 333 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 334 | { name = "konghq.com", target = "50.50.50.50", port = 80, weight = 50 }, 335 | }) 336 | local b = validate_lcb(lcb.new({ 337 | dns = client, 338 | hosts = { "konghq.com" }, 339 | })) 340 | 341 | local counts = {} 342 | local handle -- define outside loop, so it gets reused and released 343 | for i = 1,70 do 344 | local ip, _ 345 | ip, _, _, handle = b:getPeer(nil, handle) 346 | counts[ip] = (counts[ip] or 0) + 1 347 | end 348 | 349 | validate_lcb(b) 350 | 351 | local ccount = 0 352 | for i, addr in ipairs(b.addresses) do 353 | ccount = ccount + addr.connectionCount 354 | end 355 | assert.equal(1, ccount) 356 | end) 357 | 358 | end) 359 | 360 | 361 | describe("release()", function() 362 | 363 | it("releases a connection", function() 364 | dnsSRV({ 365 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 366 | }) 367 | local b = validate_lcb(lcb.new({ 368 | dns = client, 369 | hosts = { "konghq.com" }, 370 | })) 371 | 372 | local ip, _, _, handle = b:getPeer() 373 | assert.equal("20.20.20.20", ip) 374 | assert.equal(1, b.addresses[1].connectionCount) 375 | 376 | handle:release() 377 | assert.equal(0, b.addresses[1].connectionCount) 378 | end) 379 | 380 | 381 | it("releases connection of already disabled/removed address", function() 382 | dnsSRV({ 383 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 384 | }) 385 | local b = validate_lcb(lcb.new({ 386 | dns = client, 387 | hosts = { "konghq.com" }, 388 | })) 389 | 390 | local ip, _, _, handle = b:getPeer() 391 | assert.equal("20.20.20.20", ip) 392 | assert.equal(1, b.addresses[1].connectionCount) 393 | 394 | -- remove the host and its addresses 395 | b:removeHost("konghq.com") 396 | assert.equal(0, #b.addresses) 397 | 398 | local addr = handle.address 399 | handle:release() 400 | assert.equal(0, addr.connectionCount) 401 | end) 402 | 403 | end) 404 | 405 | 406 | describe("garbage collection:", function() 407 | 408 | it("releases a connection when a handle is collected", function() 409 | dnsSRV({ 410 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 411 | }) 412 | local b = validate_lcb(lcb.new({ 413 | dns = client, 414 | hosts = { "konghq.com" }, 415 | })) 416 | 417 | local ip, _, _, handle = b:getPeer() 418 | assert.equal("20.20.20.20", ip) 419 | assert.equal(1, b.addresses[1].connectionCount) 420 | 421 | local addr = handle.address 422 | handle = nil --luacheck: ignore 423 | collectgarbage() 424 | collectgarbage() 425 | 426 | assert.equal(0, addr.connectionCount) 427 | end) 428 | 429 | 430 | it("releases connection of already disabled/removed address", function() 431 | dnsSRV({ 432 | { name = "konghq.com", target = "20.20.20.20", port = 80, weight = 20 }, 433 | }) 434 | local b = validate_lcb(lcb.new({ 435 | dns = client, 436 | hosts = { "konghq.com" }, 437 | })) 438 | 439 | local ip, _, _, handle = b:getPeer() 440 | assert.equal("20.20.20.20", ip) 441 | assert.equal(1, b.addresses[1].connectionCount) 442 | 443 | -- remove the host and its addresses 444 | b:removeHost("konghq.com") 445 | assert.equal(0, #b.addresses) 446 | 447 | local addr = handle.address 448 | handle = nil --luacheck: ignore 449 | collectgarbage() 450 | collectgarbage() 451 | 452 | assert.equal(0, addr.connectionCount) 453 | end) 454 | 455 | end) 456 | 457 | end) 458 | -------------------------------------------------------------------------------- /spec/resty-runner.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env resty 2 | 3 | -- script to run Busted tests using Openresty while setting some extra flags. 4 | -- 5 | -- This script should be specified as: 6 | -- busted --lua= 7 | -- 8 | -- Alternatively specify it in the `.busted` config file 9 | 10 | 11 | -- These flags are passed to `resty` by default, to allow for more connections 12 | -- and disable the Global variable write-guard. Override it by setting the 13 | -- environment variable `BUSTED_RESTY_FLAGS`. 14 | local RESTY_FLAGS=os.getenv("BUSTED_RESTY_FLAGS") or "-c 4096 -e 'setmetatable(_G, nil)'" 15 | 16 | -- rebuild the invoked commandline, while inserting extra resty-flags 17 | local cmd = { 18 | "exec", 19 | arg[-1], 20 | RESTY_FLAGS, 21 | } 22 | for i, param in ipairs(arg) do 23 | table.insert(cmd, "'" .. param .. "'") 24 | end 25 | 26 | local _, _, rc = os.execute(table.concat(cmd, " ")) 27 | os.exit(rc) 28 | -------------------------------------------------------------------------------- /spec/test_helpers.lua: -------------------------------------------------------------------------------- 1 | -- test helper methods 2 | 3 | local _M = {} 4 | 5 | 6 | if ngx then 7 | _M.gettime = ngx.now 8 | _M.sleep = ngx.sleep 9 | else 10 | local socket = require("socket") 11 | _M.gettime = socket.gettime 12 | _M.sleep = socket.sleep 13 | end 14 | local gettime = _M.gettime 15 | 16 | 17 | -- iterator over different balancer types 18 | -- @return algorithm_name, balancer_module 19 | function _M.balancer_types() 20 | local b_types = { 21 | -- algorithm name 22 | { "consistent-hashing", "consistent_hashing" }, 23 | { "round-robin", "round_robin" }, 24 | { "least-connections", "least_connections" }, 25 | } 26 | local i = 0 27 | return function() 28 | i = i + 1 29 | if b_types[i] then 30 | return b_types[i][1], require("resty.dns.balancer." .. b_types[i][2]) 31 | end 32 | end 33 | end 34 | 35 | 36 | -- expires a record now 37 | function _M.dnsExpire(record) 38 | record.expire = gettime() - 1 39 | end 40 | 41 | 42 | -- creates an SRV record in the cache 43 | function _M.dnsSRV(client, records, staleTtl) 44 | local dnscache = client.getcache() 45 | -- if single table, then insert into a new list 46 | if not records[1] then records = { records } end 47 | 48 | for _, record in ipairs(records) do 49 | record.type = client.TYPE_SRV 50 | 51 | -- check required input 52 | assert(record.target, "target field is required for SRV record") 53 | assert(record.name, "name field is required for SRV record") 54 | assert(record.port, "port field is required for SRV record") 55 | record.name = record.name:lower() 56 | 57 | -- optionals, insert defaults 58 | record.weight = record.weight or 10 59 | record.ttl = record.ttl or 600 60 | record.priority = record.priority or 20 61 | record.class = record.class or 1 62 | end 63 | -- set timeouts 64 | records.touch = gettime() 65 | records.expire = gettime() + records[1].ttl 66 | 67 | -- create key, and insert it 68 | local key = records[1].type..":"..records[1].name 69 | dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) 70 | -- insert last-succesful lookup type 71 | dnscache:set(records[1].name, records[1].type) 72 | return records 73 | end 74 | 75 | 76 | -- creates an A record in the cache 77 | function _M.dnsA(client, records, staleTtl) 78 | local dnscache = client.getcache() 79 | -- if single table, then insert into a new list 80 | if not records[1] then records = { records } end 81 | 82 | for _, record in ipairs(records) do 83 | record.type = client.TYPE_A 84 | 85 | -- check required input 86 | assert(record.address, "address field is required for A record") 87 | assert(record.name, "name field is required for A record") 88 | record.name = record.name:lower() 89 | 90 | -- optionals, insert defaults 91 | record.ttl = record.ttl or 600 92 | record.class = record.class or 1 93 | end 94 | -- set timeouts 95 | records.touch = gettime() 96 | records.expire = gettime() + records[1].ttl 97 | 98 | -- create key, and insert it 99 | local key = records[1].type..":"..records[1].name 100 | dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) 101 | -- insert last-succesful lookup type 102 | dnscache:set(records[1].name, records[1].type) 103 | return records 104 | end 105 | 106 | 107 | -- creates an AAAA record in the cache 108 | function _M.dnsAAAA(client, records, staleTtl) 109 | local dnscache = client.getcache() 110 | -- if single table, then insert into a new list 111 | if not records[1] then records = { records } end 112 | 113 | for _, record in ipairs(records) do 114 | record.type = client.TYPE_AAAA 115 | 116 | -- check required input 117 | assert(record.address, "address field is required for AAAA record") 118 | assert(record.name, "name field is required for AAAA record") 119 | record.name = record.name:lower() 120 | 121 | -- optionals, insert defaults 122 | record.ttl = record.ttl or 600 123 | record.class = record.class or 1 124 | end 125 | -- set timeouts 126 | records.touch = gettime() 127 | records.expire = gettime() + records[1].ttl 128 | 129 | -- create key, and insert it 130 | local key = records[1].type..":"..records[1].name 131 | dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) 132 | -- insert last-succesful lookup type 133 | dnscache:set(records[1].name, records[1].type) 134 | return records 135 | end 136 | 137 | 138 | return _M 139 | -------------------------------------------------------------------------------- /spec/utils_spec.lua: -------------------------------------------------------------------------------- 1 | local dnsutils = require "resty.dns.utils" 2 | local splitlines = require("pl.stringx").splitlines 3 | local writefile = require("pl.utils").writefile 4 | local tempfilename = require("pl.path").tmpname 5 | 6 | local sleep 7 | if ngx then 8 | gettime = ngx.now -- luacheck: ignore 9 | sleep = ngx.sleep 10 | else 11 | local socket = require("socket") 12 | gettime = socket.gettime -- luacheck: ignore 13 | sleep = socket.sleep 14 | end 15 | 16 | describe("[utils]", function() 17 | 18 | describe("parsing 'hosts':", function() 19 | 20 | it("tests parsing when the 'hosts' file does not exist", function() 21 | local result, err = dnsutils.parseHosts("non/existing/file") 22 | assert.is.Nil(result) 23 | assert.is.string(err) 24 | end) 25 | 26 | it("tests parsing when the 'hosts' file is empty", function() 27 | local filename = tempfilename() 28 | writefile(filename, "") 29 | local reverse, hosts = dnsutils.parseHosts(filename) 30 | os.remove(filename) 31 | assert.is.same({}, reverse) 32 | assert.is.same({}, hosts) 33 | end) 34 | 35 | it("tests parsing 'hosts'", function() 36 | local hostsfile = splitlines( 37 | [[# The localhost entry should be in every HOSTS file and is used 38 | # to point back to yourself. 39 | 40 | 127.0.0.1 localhost 41 | ::1 localhost 42 | 43 | # My test server for the website 44 | 45 | 192.168.1.2 test.computer.com 46 | 192.168.1.3 ftp.COMPUTER.com alias1 alias2 47 | 192.168.1.4 smtp.computer.com alias3 #alias4 48 | 192.168.1.5 smtp.computer.com alias3 #doubles, first one should win 49 | 50 | #Blocking known malicious sites 51 | 127.0.0.1 admin.abcsearch.com 52 | 127.0.0.2 www3.abcsearch.com #[Browseraid] 53 | 127.0.0.3 www.abcsearch.com wwwsearch #[Restricted Zone site] 54 | 55 | [::1] alsolocalhost #support IPv6 in brackets 56 | ]]) 57 | local reverse, hosts = dnsutils.parseHosts(hostsfile) 58 | assert.is.equal(hosts[1].ip, "127.0.0.1") 59 | assert.is.equal(hosts[1].canonical, "localhost") 60 | assert.is.Nil(hosts[1][1]) -- no aliases 61 | assert.is.Nil(hosts[1][2]) 62 | assert.is.equal("127.0.0.1", reverse.localhost.ipv4) 63 | assert.is.equal("[::1]", reverse.localhost.ipv6) 64 | 65 | assert.is.equal(hosts[2].ip, "[::1]") 66 | assert.is.equal(hosts[2].canonical, "localhost") 67 | 68 | assert.is.equal(hosts[3].ip, "192.168.1.2") 69 | assert.is.equal(hosts[3].canonical, "test.computer.com") 70 | assert.is.Nil(hosts[3][1]) -- no aliases 71 | assert.is.Nil(hosts[3][2]) 72 | assert.is.equal("192.168.1.2", reverse["test.computer.com"].ipv4) 73 | 74 | assert.is.equal(hosts[4].ip, "192.168.1.3") 75 | assert.is.equal(hosts[4].canonical, "ftp.computer.com") -- converted to lowercase! 76 | assert.is.equal(hosts[4][1], "alias1") 77 | assert.is.equal(hosts[4][2], "alias2") 78 | assert.is.Nil(hosts[4][3]) 79 | assert.is.equal("192.168.1.3", reverse["ftp.computer.com"].ipv4) 80 | assert.is.equal("192.168.1.3", reverse["alias1"].ipv4) 81 | assert.is.equal("192.168.1.3", reverse["alias2"].ipv4) 82 | 83 | assert.is.equal(hosts[5].ip, "192.168.1.4") 84 | assert.is.equal(hosts[5].canonical, "smtp.computer.com") 85 | assert.is.equal(hosts[5][1], "alias3") 86 | assert.is.Nil(hosts[5][2]) 87 | assert.is.equal("192.168.1.4", reverse["smtp.computer.com"].ipv4) 88 | assert.is.equal("192.168.1.4", reverse["alias3"].ipv4) 89 | 90 | assert.is.equal(hosts[6].ip, "192.168.1.5") 91 | assert.is.equal(hosts[6].canonical, "smtp.computer.com") 92 | assert.is.equal(hosts[6][1], "alias3") 93 | assert.is.Nil(hosts[6][2]) 94 | assert.is.equal("192.168.1.4", reverse["smtp.computer.com"].ipv4) -- .1.4; first one wins! 95 | assert.is.equal("192.168.1.4", reverse["alias3"].ipv4) -- .1.4; first one wins! 96 | 97 | assert.is.equal(hosts[10].ip, "[::1]") 98 | assert.is.equal(hosts[10].canonical, "alsolocalhost") 99 | assert.is.equal(hosts[10].family, "ipv6") 100 | assert.is.equal("[::1]", reverse["alsolocalhost"].ipv6) 101 | end) 102 | 103 | end) 104 | 105 | describe("parsing 'resolv.conf':", function() 106 | 107 | -- override os.getenv to insert env variables 108 | local old_getenv = os.getenv 109 | local envvars -- whatever is in this table, gets served first 110 | before_each(function() 111 | envvars = {} 112 | os.getenv = function(name) -- luacheck: ignore 113 | return envvars[name] or old_getenv(name) 114 | end 115 | end) 116 | 117 | after_each(function() 118 | os.getenv = old_getenv -- luacheck: ignore 119 | envvars = nil 120 | end) 121 | 122 | it("tests parsing when the 'resolv.conf' file does not exist", function() 123 | local result, err = dnsutils.parseResolvConf("non/existing/file") 124 | assert.is.Nil(result) 125 | assert.is.string(err) 126 | end) 127 | 128 | it("tests parsing when the 'resolv.conf' file is empty", function() 129 | local filename = tempfilename() 130 | writefile(filename, "") 131 | local resolv, err = dnsutils.parseResolvConf(filename) 132 | os.remove(filename) 133 | assert.is.same({}, resolv) 134 | assert.is.Nil(err) 135 | end) 136 | 137 | it("tests parsing 'resolv.conf' with multiple comment types", function() 138 | local file = splitlines( 139 | [[# this is just a comment line 140 | # at the top of the file 141 | 142 | domain myservice.com 143 | 144 | nameserver 8.8.8.8 145 | nameserver 2602:306:bca8:1ac0::1 ; and a comment here 146 | nameserver 8.8.8.8:1234 ; this one has a port number (limited systems support this) 147 | nameserver 1.2.3.4 ; this one is 4th, so should be ignored 148 | 149 | # search is commented out, test below for a mutually exclusive one 150 | #search domaina.com domainb.com 151 | 152 | sortlist list1 list2 #list3 is not part of it 153 | 154 | options ndots:2 155 | options timeout:3 156 | options attempts:4 157 | 158 | options debug 159 | options rotate ; let's see about a comment here 160 | options no-check-names 161 | options inet6 162 | ; here's annother comment 163 | options ip6-bytestring 164 | options ip6-dotint 165 | options no-ip6-dotint 166 | options edns0 167 | options single-request 168 | options single-request-reopen 169 | options no-tld-query 170 | options use-vc 171 | ]]) 172 | local resolv, err = dnsutils.parseResolvConf(file) 173 | assert.is.Nil(err) 174 | assert.is.equal("myservice.com", resolv.domain) 175 | assert.is.same({ "8.8.8.8", "2602:306:bca8:1ac0::1", "8.8.8.8:1234" }, resolv.nameserver) 176 | assert.is.same({ "list1", "list2" }, resolv.sortlist) 177 | assert.is.same({ ndots = 2, timeout = 3, attempts = 4, debug = true, rotate = true, 178 | ["no-check-names"] = true, inet6 = true, ["ip6-bytestring"] = true, 179 | ["ip6-dotint"] = nil, -- overridden by the next one, mutually exclusive 180 | ["no-ip6-dotint"] = true, edns0 = true, ["single-request"] = true, 181 | ["single-request-reopen"] = true, ["no-tld-query"] = true, ["use-vc"] = true}, 182 | resolv.options) 183 | end) 184 | 185 | it("tests parsing 'resolv.conf' with mutual exclusive domain vs search", function() 186 | local file = splitlines( 187 | [[domain myservice.com 188 | 189 | # search is overriding domain above 190 | search domaina.com domainb.com 191 | 192 | ]]) 193 | local resolv, err = dnsutils.parseResolvConf(file) 194 | assert.is.Nil(err) 195 | assert.is.Nil(resolv.domain) 196 | assert.is.same({ "domaina.com", "domainb.com" }, resolv.search) 197 | end) 198 | 199 | it("tests parsing 'resolv.conf' with max search entries MAXSEARCH", function() 200 | local file = splitlines( 201 | [[ 202 | 203 | search domain1.com domain2.com domain3.com domain4.com domain5.com domain6.com domain7.com 204 | 205 | ]]) 206 | local resolv, err = dnsutils.parseResolvConf(file) 207 | assert.is.Nil(err) 208 | assert.is.Nil(resolv.domain) 209 | assert.is.same({ 210 | "domain1.com", 211 | "domain2.com", 212 | "domain3.com", 213 | "domain4.com", 214 | "domain5.com", 215 | "domain6.com", 216 | }, resolv.search) 217 | end) 218 | 219 | it("tests parsing 'resolv.conf' with environment variables", function() 220 | local file = splitlines( 221 | [[# this is just a comment line 222 | domain myservice.com 223 | 224 | nameserver 8.8.8.8 225 | nameserver 8.8.4.4 ; and a comment here 226 | 227 | options ndots:1 228 | ]]) 229 | local resolv, err = dnsutils.parseResolvConf(file) 230 | assert.is.Nil(err) 231 | 232 | envvars.LOCALDOMAIN = "domaina.com domainb.com" 233 | envvars.RES_OPTIONS = "ndots:2 debug" 234 | resolv = dnsutils.applyEnv(resolv) 235 | 236 | assert.is.Nil(resolv.domain) -- must be nil, mutually exclusive 237 | assert.is.same({ "domaina.com", "domainb.com" }, resolv.search) 238 | 239 | assert.is.same({ ndots = 2, debug = true }, resolv.options) 240 | end) 241 | 242 | it("tests parsing 'resolv.conf' with non-existing environment variables", function() 243 | local file = splitlines( 244 | [[# this is just a comment line 245 | domain myservice.com 246 | 247 | nameserver 8.8.8.8 248 | nameserver 8.8.4.4 ; and a comment here 249 | 250 | options ndots:2 251 | ]]) 252 | local resolv, err = dnsutils.parseResolvConf(file) 253 | assert.is.Nil(err) 254 | 255 | envvars.LOCALDOMAIN = "" 256 | envvars.RES_OPTIONS = "" 257 | resolv = dnsutils.applyEnv(resolv) 258 | 259 | assert.is.equals("myservice.com", resolv.domain) -- must be nil, mutually exclusive 260 | 261 | assert.is.same({ ndots = 2 }, resolv.options) 262 | end) 263 | 264 | it("tests pass-through error handling of 'applyEnv'", function() 265 | local fname = "non/existing/file" 266 | local r1, e1 = dnsutils.parseResolvConf(fname) 267 | local r2, e2 = dnsutils.applyEnv(dnsutils.parseResolvConf(fname)) 268 | assert.are.same(r1, r2) 269 | assert.are.same(e1, e2) 270 | end) 271 | 272 | end) 273 | 274 | describe("cached versions", function() 275 | 276 | local utils = require("pl.utils") 277 | local oldreadlines = utils.readlines 278 | 279 | before_each(function() 280 | utils.readlines = function(name) 281 | if name:match("hosts") then 282 | return { -- hosts file 283 | "127.0.0.1 localhost", 284 | "192.168.1.2 test.computer.com", 285 | "192.168.1.3 ftp.computer.com alias1 alias2", 286 | } 287 | else 288 | return { -- resolv.conf file 289 | "domain myservice.com", 290 | "nameserver 8.8.8.8 ", 291 | } 292 | end 293 | end 294 | end) 295 | 296 | after_each(function() 297 | utils.readlines = oldreadlines 298 | end) 299 | 300 | it("tests caching the hosts file", function() 301 | local val1r, val1 = dnsutils.getHosts() 302 | local val2r, val2 = dnsutils.getHosts() 303 | assert.Not.equal(val1, val2) -- no ttl specified, so distinct tables 304 | assert.Not.equal(val1r, val2r) -- no ttl specified, so distinct tables 305 | 306 | val1r, val1 = dnsutils.getHosts(1) 307 | val2r, val2 = dnsutils.getHosts() 308 | assert.are.equal(val1, val2) -- ttl specified, so same tables 309 | assert.are.equal(val1r, val2r) -- ttl specified, so same tables 310 | 311 | -- wait for cache to expire 312 | sleep(2) 313 | 314 | val2r, val2 = dnsutils.getHosts() 315 | assert.Not.equal(val1, val2) -- ttl timed out, so distinct tables 316 | assert.Not.equal(val1r, val2r) -- ttl timed out, so distinct tables 317 | end) 318 | 319 | it("tests caching the resolv.conf file & variables", function() 320 | local val1 = dnsutils.getResolv() 321 | local val2 = dnsutils.getResolv() 322 | assert.Not.equal(val1, val2) -- no ttl specified, so distinct tables 323 | 324 | val1 = dnsutils.getResolv(1) 325 | val2 = dnsutils.getResolv() 326 | assert.are.equal(val1, val2) -- ttl specified, so same tables 327 | 328 | -- wait for cache to expire 329 | sleep(2) 330 | 331 | val2 = dnsutils.getResolv() 332 | assert.Not.equal(val1, val2) -- ttl timed out, so distinct tables 333 | end) 334 | 335 | end) 336 | 337 | describe("hostnameType", function() 338 | -- no check on "name" type as anything not ipv4 and not ipv6 will be labelled as 'name' anyway 339 | it("checks valid IPv4 address types", function() 340 | assert.are.same("ipv4", dnsutils.hostnameType("123.123.123.123")) 341 | assert.are.same("ipv4", dnsutils.hostnameType("1.2.3.4")) 342 | end) 343 | it("checks valid IPv6 address types", function() 344 | assert.are.same("ipv6", dnsutils.hostnameType("::1")) 345 | assert.are.same("ipv6", dnsutils.hostnameType("2345::6789")) 346 | assert.are.same("ipv6", dnsutils.hostnameType("0001:0001:0001:0001:0001:0001:0001:0001")) 347 | end) 348 | it("checks valid FQDN address types", function() 349 | assert.are.same("name", dnsutils.hostnameType("konghq.")) 350 | assert.are.same("name", dnsutils.hostnameType("konghq.com.")) 351 | assert.are.same("name", dnsutils.hostnameType("www.konghq.com.")) 352 | end) 353 | end) 354 | 355 | describe("parseHostname", function() 356 | it("parses valid IPv4 address types", function() 357 | assert.are.same({"123.123.123.123", nil, "ipv4"}, {dnsutils.parseHostname("123.123.123.123")}) 358 | assert.are.same({"1.2.3.4", 567, "ipv4"}, {dnsutils.parseHostname("1.2.3.4:567")}) 359 | end) 360 | it("parses valid IPv6 address types", function() 361 | assert.are.same({"[::1]", nil, "ipv6"}, {dnsutils.parseHostname("::1")}) 362 | assert.are.same({"[::1]", nil, "ipv6"}, {dnsutils.parseHostname("[::1]")}) 363 | assert.are.same({"[::1]", 123, "ipv6"}, {dnsutils.parseHostname("[::1]:123")}) 364 | assert.are.same({"[2345::6789]", nil, "ipv6"}, {dnsutils.parseHostname("2345::6789")}) 365 | assert.are.same({"[2345::6789]", nil, "ipv6"}, {dnsutils.parseHostname("[2345::6789]")}) 366 | assert.are.same({"[2345::6789]", 321, "ipv6"}, {dnsutils.parseHostname("[2345::6789]:321")}) 367 | end) 368 | it("parses valid name address types", function() 369 | assert.are.same({"somename", nil, "name"}, {dnsutils.parseHostname("somename")}) 370 | assert.are.same({"somename", 123, "name"}, {dnsutils.parseHostname("somename:123")}) 371 | assert.are.same({"somename456", nil, "name"}, {dnsutils.parseHostname("somename456")}) 372 | assert.are.same({"somename456", 123, "name"}, {dnsutils.parseHostname("somename456:123")}) 373 | assert.are.same({"somename456.domain.local789", nil, "name"}, {dnsutils.parseHostname("somename456.domain.local789")}) 374 | assert.are.same({"somename456.domain.local789", 123, "name"}, {dnsutils.parseHostname("somename456.domain.local789:123")}) 375 | end) 376 | it("parses valid FQDN address types", function() 377 | assert.are.same({"somename.", nil, "name"}, {dnsutils.parseHostname("somename.")}) 378 | assert.are.same({"somename.", 123, "name"}, {dnsutils.parseHostname("somename.:123")}) 379 | assert.are.same({"somename456.", nil, "name"}, {dnsutils.parseHostname("somename456.")}) 380 | assert.are.same({"somename456.", 123, "name"}, {dnsutils.parseHostname("somename456.:123")}) 381 | assert.are.same({"somename456.domain.local789.", nil, "name"}, {dnsutils.parseHostname("somename456.domain.local789.")}) 382 | assert.are.same({"somename456.domain.local789.", 123, "name"}, {dnsutils.parseHostname("somename456.domain.local789.:123")}) 383 | end) 384 | end) 385 | 386 | end) 387 | -------------------------------------------------------------------------------- /src/resty/dns/balancer/consistent_hashing.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------- 2 | -- Consistent-Hashing balancer 3 | -- 4 | -- This balancer implements a consistent-hashing algorithm based on the 5 | -- Ketama algorithm. 6 | -- 7 | -- This load balancer is designed to make sure that every time a load 8 | -- balancer object is built, it is built the same, no matter the order the 9 | -- process is done. 10 | -- 11 | -- __NOTE:__ This documentation only described the altered user 12 | -- methods/properties, see the `user properties` from the `balancer_base` 13 | -- for a complete overview. 14 | -- 15 | -- @author Vinicius Mignot 16 | -- @copyright 2020 Kong Inc. All rights reserved. 17 | -- @license Apache 2.0 18 | 19 | 20 | local balancer_base = require "resty.dns.balancer.base" 21 | local xxhash32 = require "luaxxhash" 22 | 23 | local floor = math.floor 24 | local ngx_log = ngx.log 25 | local ngx_CRIT = ngx.CRIT 26 | local ngx_DEBUG = ngx.DEBUG 27 | local ngx_ERR = ngx.ERR 28 | local table_sort = table.sort 29 | 30 | 31 | -- constants 32 | local DEFAULT_CONTINUUM_SIZE = 1000 33 | local MAX_CONTINUUM_SIZE = 2^32 34 | local MIN_CONTINUUM_SIZE = 1000 35 | local SERVER_POINTS = 160 -- number of points when all targets have same weight 36 | local SEP = " " -- string separator to be used when hashing hostnames 37 | 38 | 39 | local _M = {} 40 | local consistent_hashing = {} 41 | 42 | 43 | -- returns the index a value will point to in a generic continuum, based on 44 | -- continuum size 45 | local function get_continuum_index(value, points) 46 | return ((xxhash32(tostring(value)) % points) + 1) 47 | end 48 | 49 | 50 | -- hosts and addresses must be sorted lexically before adding to the continuum, 51 | -- so they are added always in the same order. This makes sure that collisions 52 | -- will be treated always the same way. 53 | local function sort_hosts_and_addresses(balancer) 54 | if type(balancer) ~= "table" then 55 | error("balancer must be a table") 56 | end 57 | 58 | if balancer.hosts == nil then 59 | return 60 | end 61 | 62 | table_sort(balancer.hosts, function(a, b) 63 | local ta = tostring(a.hostname) 64 | local tb = tostring(b.hostname) 65 | return ta < tb or (ta == tb and tonumber(a.port) < tonumber(b.port)) 66 | end) 67 | 68 | for _, host in ipairs(balancer.hosts) do 69 | table_sort(host.addresses, function(a, b) 70 | return (tostring(a.ip) .. ":" .. tostring(a.port)) < 71 | (tostring(b.ip) .. ":" .. tostring(b.port)) 72 | end) 73 | end 74 | 75 | end 76 | 77 | 78 | --- Adds a host to the balancer. 79 | -- This function checks if there is enough points to add more hosts and 80 | -- then call the base class's `addHost()`. 81 | -- see `addHost()` from the `balancer_base` for more details. 82 | function consistent_hashing:addHost(hostname, port, weight) 83 | local host_count = #self.hosts + 1 84 | 85 | if (host_count * SERVER_POINTS) >= self.points then 86 | ngx_log(ngx_ERR, self.log_prefix, "consistent hashing balancer requires ", 87 | "more entries to be able to add the number of hosts requested, ", 88 | "please increase the wheel size") 89 | return nil, "not enough free slots to add more hosts" 90 | end 91 | 92 | self.super.addHost(self, hostname, port, weight) 93 | 94 | return self 95 | end 96 | 97 | 98 | --- Actually adds the addresses to the continuum. 99 | -- This function should not be called directly, as it will called by 100 | -- `addHost()` after adding the new host. 101 | -- This function makes sure the continuum will be built identically every 102 | -- time, no matter the order the hosts are added. 103 | function consistent_hashing:afterHostUpdate(host) 104 | local points = self.points 105 | local new_continuum = {} 106 | local total_weight = self.weight 107 | local host_count = #self.hosts 108 | local total_collision = 0 109 | 110 | sort_hosts_and_addresses(self) 111 | 112 | for weight, address, h in self:addressIter() do 113 | local addr_prop = weight / total_weight 114 | local entries = floor(addr_prop * host_count * SERVER_POINTS) 115 | if weight > 0 and entries == 0 then 116 | entries = 1 -- every address with weight > 0 must have at least one entry 117 | end 118 | local port = address.port and ":" .. tostring(address.port) or "" 119 | local i = 1 120 | while i <= entries do 121 | local name = tostring(address.ip) .. ":" .. port .. SEP .. tostring(i) 122 | local index = get_continuum_index(name, points) 123 | if new_continuum[index] == nil then 124 | new_continuum[index] = address 125 | else 126 | entries = entries + 1 -- move the problem forward 127 | total_collision = total_collision + 1 128 | end 129 | i = i + 1 130 | if i > self.points then 131 | -- this should happen only if there are an awful amount of hosts with 132 | -- low relative weight. 133 | ngx_log(ngx_CRIT, "consistent hashing balancer requires more entries ", 134 | "to add the number of hosts requested, please increase the ", 135 | "wheel size") 136 | return 137 | end 138 | end 139 | end 140 | 141 | ngx_log(ngx_DEBUG, self.log_prefix, "continuum of size ", self.points, 142 | " updated with ", total_collision, " collisions") 143 | 144 | self.continuum = new_continuum 145 | 146 | end 147 | 148 | 149 | --- Gets an IP/port/hostname combo for the value to hash 150 | -- This function will hash the `valueToHash` param and use it as an index 151 | -- in the continuum. It will return the address that is at the hashed 152 | -- value or the first one found going counter-clockwise in the continuum. 153 | -- @param cacheOnly If truthy, no dns lookups will be done, only cache. 154 | -- @param handle the `handle` returned by a previous call to `getPeer`. 155 | -- This will retain some state over retries. See also `setAddressStatus`. 156 | -- @param valueToHash value for consistent hashing. Please note that this 157 | -- value will be hashed, so no need to hash it prior to calling this 158 | -- function. 159 | -- @return `ip + port + hostheader` + `handle`, or `nil+error` 160 | function consistent_hashing:getPeer(cacheOnly, handle, valueToHash) 161 | ngx_log(ngx_DEBUG, self.log_prefix, "trying to get peer with value to hash: [", 162 | valueToHash, "]") 163 | if not self.healthy then 164 | return nil, balancer_base.errors.ERR_BALANCER_UNHEALTHY 165 | end 166 | 167 | if handle then 168 | -- existing handle, so it's a retry 169 | handle.retryCount = handle.retryCount + 1 170 | else 171 | -- no handle, so this is a first try 172 | handle = self:getHandle() -- no GC specific handler needed 173 | handle.retryCount = 0 174 | end 175 | 176 | if not handle.hashValue then 177 | if not valueToHash then 178 | error("can't getPeer with no value to hash", 2) 179 | end 180 | handle.hashValue = get_continuum_index(valueToHash, self.points) 181 | end 182 | 183 | local address 184 | local index = handle.hashValue 185 | local ip, port, hostname 186 | while (index - 1) ~= handle.hashValue do 187 | if index == 0 then 188 | index = self.points 189 | end 190 | 191 | address = self.continuum[index] 192 | if address ~= nil and address.available and not address.disabled then 193 | ip, port, hostname = address:getPeer(cacheOnly) 194 | if ip then 195 | -- success, update handle 196 | handle.address = address 197 | return ip, port, hostname, handle 198 | 199 | elseif port == balancer_base.errors.ERR_DNS_UPDATED then 200 | -- we just need to retry the same index, no change for 'pointer', just 201 | -- in case of dns updates, we need to check our health again. 202 | if not self.healthy then 203 | return nil, balancer_base.errors.ERR_BALANCER_UNHEALTHY 204 | end 205 | elseif port == balancer_base.errors.ERR_ADDRESS_UNAVAILABLE then 206 | ngx_log(ngx_DEBUG, self.log_prefix, "found address but it was unavailable. ", 207 | " trying next one.") 208 | else 209 | -- an unknown error occured 210 | return nil, port 211 | end 212 | 213 | end 214 | 215 | index = index - 1 216 | end 217 | 218 | return nil, balancer_base.errors.ERR_NO_PEERS_AVAILABLE 219 | end 220 | 221 | --- Creates a new balancer. 222 | -- 223 | -- The balancer is based on a wheel (continuum) with a number of points 224 | -- between MIN_CONTINUUM_SIZE and MAX_CONTINUUM_SIZE points. Key points 225 | -- will be assigned to addresses based on their IP and port. The number 226 | -- of points each address will be assigned is proportional to their weight. 227 | -- 228 | -- The options table has the following fields, additional to the ones from 229 | -- the `balancer_base`: 230 | -- 231 | -- - `hosts` (optional) containing hostnames, ports, and weights. If 232 | -- omitted, ports and weights default respectively to 80 and 10. The list 233 | -- will be sorted before being added, so the order of entry is 234 | -- deterministic. 235 | -- - `wheelSize` (optional) for total number of positions in the 236 | -- continuum. If omitted `DEFAULT_CONTINUUM_SIZE` is used. It is important 237 | -- to have enough indices to fit all addresses entries, keep in mind that 238 | -- each address will use 160 entries in the continuum (more or less, 239 | -- proportional to its weight, but the total points will always be 240 | -- `160 * addresses`). Consider the maximum number of targets expected, as 241 | -- new hosts can be dynamically added, and DNS renewals might yield 242 | -- larger record sets. The `wheelSize` cannot be altered, the object has 243 | -- to built again to change this value. On a similar note, making it too 244 | -- big will have a performance impact to get peers from the continuum, as 245 | -- the values will be too dispersed among them. 246 | -- @param opts table with options 247 | -- @return new balancer object or nil+error 248 | function _M.new(opts) 249 | assert(type(opts) == "table", "Expected an options table, but got: "..type(opts)) 250 | if not opts.log_prefix then 251 | opts.log_prefix = "hash-lb" 252 | end 253 | 254 | local self = assert(balancer_base.new(opts)) 255 | 256 | self.continuum = {} 257 | self.points = (opts.wheelSize and 258 | opts.wheelSize >= MIN_CONTINUUM_SIZE and 259 | opts.wheelSize <= MAX_CONTINUUM_SIZE) and 260 | opts.wheelSize or DEFAULT_CONTINUUM_SIZE 261 | 262 | -- inject overridden methods 263 | for name, method in pairs(consistent_hashing) do 264 | self[name] = method 265 | end 266 | 267 | for _, host in ipairs(opts.hosts or {}) do 268 | local new_host = type(host) == "table" and host or { name = host } 269 | local ok, err = self:addHost(new_host.name, new_host.port, new_host.weight) 270 | if not ok then 271 | return ok, "Failed creating a balancer: " .. tostring(err) 272 | end 273 | end 274 | 275 | ngx_log(ngx_DEBUG, self.log_prefix, "consistent_hashing balancer created") 276 | 277 | return self 278 | end 279 | 280 | 281 | -------------------------------------------------------------------------------- 282 | -- for testing only 283 | 284 | function consistent_hashing:_get_continuum() 285 | return self.continuum 286 | end 287 | 288 | 289 | function consistent_hashing:_hit_all() 290 | for _, address in pairs(self.continuum) do 291 | if address.host then 292 | address:getPeer() 293 | end 294 | end 295 | end 296 | 297 | 298 | 299 | return _M 300 | -------------------------------------------------------------------------------- /src/resty/dns/balancer/handle.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- Handle module. 3 | -- 4 | -- Implements handles to be used by the `objBalancer:getPeer` method. These 5 | -- implement a __gc method for tracking statistics and not leaking resources 6 | -- in case a connection gets aborted prematurely. 7 | -- 8 | -- This module is only relevant when implementing your own balancer 9 | -- algorithms. 10 | -- 11 | -- @author Thijs Schreijer 12 | -- @copyright 2016-2020 Kong Inc. All rights reserved. 13 | -- @license Apache 2.0 14 | 15 | 16 | local table_new = require "table.new" 17 | local table_clear = require "table.clear" 18 | local EMPTY = setmetatable({}, 19 | {__newindex = function() error("The 'EMPTY' table is read-only") end}) 20 | 21 | 22 | local cache_max = 1000 23 | local cache_count = 0 24 | local cache = table_new(cache_max, 0) 25 | 26 | 27 | local _M = {} 28 | 29 | 30 | local createHandle 31 | do 32 | local function udata_gc_method(self) 33 | -- find our handle 34 | local mt = getmetatable(self) 35 | local handle = mt.handle 36 | -- disconnect handle and udata 37 | mt.handle = nil 38 | handle.__udata = nil 39 | -- find __gc method 40 | local __gc = (handle or EMPTY).__gc 41 | if not __gc then 42 | return 43 | end 44 | -- execute __gc method 45 | __gc(handle) 46 | end 47 | 48 | function createHandle(__gc) 49 | -- create handle 50 | local handle = { 51 | __gc = __gc 52 | } 53 | -- create userdata 54 | local __udata = newproxy(true) 55 | local mt = getmetatable(__udata) 56 | mt.__gc = udata_gc_method 57 | -- connect handle and userdata 58 | mt.handle = handle 59 | handle.__udata = __udata 60 | 61 | return handle 62 | end 63 | end 64 | 65 | --- Gets a handle from the cache. 66 | -- The handle comes from the cache or it is newly created. A handle is just a 67 | -- table. It will have two special fields: 68 | -- 69 | -- - `__udata`: (read-only) a userdata used to track the lifetime of the handle 70 | -- - `__gc`: (read/write) this method will be called on GC. 71 | -- 72 | -- __NOTE__: the `__gc` will only be called when the handle is garbage collected, 73 | -- not when it is returned by calling `release`. 74 | -- @param __gc (optional, function) the method called when the handle is GC'ed. 75 | -- @return handle 76 | -- @usage 77 | -- local handle = _M 78 | -- 79 | -- local my_gc_handler = function(self) 80 | -- print(self.name .. " was deleted") 81 | -- end 82 | -- 83 | -- local h1 = handle.get(my_gc_handler) 84 | -- h1.name = "Obama" 85 | -- local h2 = handle.get(my_gc_handler) 86 | -- h2.name = "Trump" 87 | -- 88 | -- handle.release(h1) -- explicitly release it 89 | -- h1 = nil 90 | -- h2 = nil -- not released, will be GC'ed 91 | -- collectgarbage() 92 | -- collectgarbage() --> "Trump was deleted" 93 | function _M.get(__gc) 94 | if cache_count == 0 then 95 | -- cache is empty, create a new one 96 | return createHandle(__gc) 97 | end 98 | local handle = cache[cache_count] 99 | cache[cache_count] = nil 100 | cache_count = cache_count - 1 101 | handle.__gc = __gc 102 | return handle 103 | end 104 | 105 | --- Returns a handle to the cache. 106 | -- The handle will be cleared, returned to the cache, and its `__gc` handle 107 | -- will NOT be called. 108 | -- @param handle the handle to return to the cache 109 | -- @return nothing 110 | function _M.release(handle) 111 | local __udata = handle.__udata 112 | if not __udata then 113 | -- this one was GC'ed, we check this because we do not want 114 | -- to accidentally ressurect a handle. 115 | return 116 | end 117 | if cache_count >= cache_max then 118 | -- we're dropping this one, our cache is full 119 | handle.__udata = nil 120 | handle.__gc = nil 121 | return 122 | end 123 | -- return it to the cache 124 | table_clear(handle) 125 | handle.__udata = __udata 126 | cache_count = cache_count + 1 127 | cache[cache_count] = handle 128 | end 129 | 130 | 131 | --- Sets a new cache size. The default size is 1000. 132 | -- @param size the new size. 133 | -- @return nothing, or throws an error on bad input 134 | function _M.setCacheSize(size) 135 | assert(type(size) == "number", "expected a number") 136 | assert(size >= 0, "expected size >= 0") 137 | cache_max = size 138 | local new_cache = table_new(cache_max, 0) 139 | cache_count = math.min(cache_count, size) 140 | for i = 1, cache_count do 141 | new_cache[i] = cache[i] 142 | end 143 | cache = new_cache 144 | end 145 | 146 | 147 | return _M 148 | -------------------------------------------------------------------------------- /src/resty/dns/balancer/least_connections.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------- 2 | -- Least-connections balancer. 3 | -- 4 | -- This balancer implements a least-connections algorithm. The balancer will 5 | -- honour the weights. See the base-balancer for details on how the weights 6 | -- are set. 7 | -- 8 | -- __NOTE:__ This documentation only described the altered user methods/properties 9 | -- from the base-balancer. See the `user properties` from the `balancer_base` for a 10 | -- complete overview. 11 | -- 12 | -- @author Thijs Schreijer 13 | -- @copyright 2016-2020 Kong Inc. All rights reserved. 14 | -- @license Apache 2.0 15 | 16 | 17 | local balancer_base = require "resty.dns.balancer.base" 18 | local binaryHeap = require "binaryheap" 19 | local ngx_log = ngx.log 20 | local ngx_DEBUG = ngx.DEBUG 21 | 22 | local EMPTY = setmetatable({}, 23 | {__newindex = function() error("The 'EMPTY' table is read-only") end}) 24 | 25 | local _M = {} 26 | local lc = {} 27 | local lcAddr = {} 28 | 29 | 30 | -- @param delta number (+1 or -1) to update the connection count 31 | function lcAddr:updateConnectionCount(delta) 32 | self.connectionCount = self.connectionCount + delta 33 | 34 | if not self.available then 35 | return 36 | end 37 | 38 | -- go update the heap position 39 | local bh = ((self.host or EMPTY).balancer or EMPTY).binaryHeap 40 | if bh then 41 | -- NOTE: we use `connectionCount + 1` this ensures that even on a balancer 42 | -- with 0 connections the heighest weighted entry is picked first. If we'd 43 | -- not add the `+1` then any target with 0 connections would always be the 44 | -- first to be picked (even if it has a very low eight) 45 | bh:update(self, (self.connectionCount + 1) / self.weight) 46 | end 47 | end 48 | 49 | 50 | function lcAddr:release(handle, ignore) 51 | self:updateConnectionCount(-1) 52 | end 53 | 54 | 55 | function lcAddr:setState(available) 56 | local old_available = self.available 57 | self.super.setState(self, available) 58 | if old_available == self.available then 59 | -- nothing changed 60 | return 61 | end 62 | 63 | local bh = self.host.balancer.binaryHeap 64 | if self.available then 65 | bh:insert((self.connectionCount + 1) / self.weight, self) 66 | else 67 | bh:remove(self) 68 | end 69 | end 70 | 71 | 72 | -- disabling the address, so delete from binaryHeap 73 | function lcAddr:disable() 74 | self.host.balancer.binaryHeap:remove(self) 75 | self.super.disable(self) 76 | end 77 | 78 | 79 | function lc:newAddress(addr) 80 | addr = self.super.newAddress(self, addr) 81 | 82 | -- inject additional properties 83 | addr.connectionCount = 0 84 | 85 | -- inject additioanl methods 86 | for name, method in pairs(lcAddr) do 87 | addr[name] = method 88 | end 89 | 90 | -- insert self in binary heap 91 | if addr.available then 92 | self.binaryHeap:insert((addr.connectionCount + 1) / addr.weight, addr) 93 | end 94 | 95 | return addr 96 | end 97 | 98 | 99 | 100 | function lc:getPeer(cacheOnly, handle, hashValue) 101 | if handle then 102 | -- existing handle, so it's a retry 103 | handle.retryCount = handle.retryCount + 1 104 | 105 | -- keep track of failed addresses 106 | handle.failedAddresses = handle.failedAddresses or setmetatable({}, {__mode = "k"}) 107 | handle.failedAddresses[handle.address] = true 108 | -- let go of previous connection 109 | handle.address:release() 110 | handle.address = nil 111 | else 112 | -- no handle, so this is a first try 113 | handle = self:getHandle() -- no specific GC method required 114 | handle.retryCount = 0 115 | end 116 | 117 | local address, ip, port, host 118 | while true do 119 | if not self.healthy then 120 | -- Balancer unhealthy, nothing we can do. 121 | -- This check must be inside the loop, since calling getPeer could 122 | -- cause a DNS update. 123 | ip, port, host = nil, self.errors.ERR_BALANCER_UNHEALTHY, nil 124 | break 125 | end 126 | 127 | 128 | -- go and find the next `address` object according to the LB policy 129 | do 130 | local reinsert 131 | repeat 132 | if address then 133 | -- this address we failed before, so temp store it and pop it from 134 | -- the tree. When we're done we'll reinsert them. 135 | reinsert = reinsert or {} 136 | reinsert[#reinsert + 1] = address 137 | self.binaryHeap:pop() 138 | end 139 | address = self.binaryHeap:peek() 140 | until address == nil or not (handle.failedAddresses or EMPTY)[address] 141 | 142 | if address == nil and handle.failedAddresses then 143 | -- we failed all addresses, so drop the list of failed ones, we are trying 144 | -- again, so we restart using the ones that previously failed us. Until 145 | -- eventually we hit the limit of retries (but that's up to the user). 146 | handle.failedAddresses = nil 147 | address = reinsert[1] -- the address to use is the first one, top of the heap 148 | end 149 | 150 | if reinsert then 151 | -- reinsert the ones we temporarily popped 152 | for i = 1, #reinsert do 153 | local addr = reinsert[i] 154 | self.binaryHeap:insert((addr.connectionCount + 1) / addr.weight, addr) 155 | end 156 | reinsert = nil -- luacheck: ignore 157 | end 158 | end 159 | 160 | 161 | -- check the address returned, and get an IP 162 | 163 | if address == nil then 164 | -- No peers are available 165 | ip, port, host = nil, self.errors.ERR_NO_PEERS_AVAILABLE, nil 166 | break 167 | end 168 | 169 | ip, port, host = address:getPeer(cacheOnly) 170 | if ip then 171 | -- success, exit 172 | handle.address = address 173 | address:updateConnectionCount(1) 174 | break 175 | end 176 | 177 | if port ~= self.errors.ERR_DNS_UPDATED then 178 | -- an unknown error 179 | break 180 | end 181 | 182 | -- if here, we're going to retry because we already tried this address, 183 | -- or because of a dns update 184 | end 185 | 186 | if ip then 187 | return ip, port, host, handle 188 | else 189 | handle.address = nil 190 | handle:release(true) 191 | return nil, port 192 | end 193 | end 194 | 195 | 196 | --- Creates a new balancer. The balancer is based on a binary heap tracking 197 | -- the number of active connections. The number of connections 198 | -- assigned will be relative to the weight. 199 | -- 200 | -- The options table has the following fields, additional to the ones from 201 | -- the `balancer_base`; 202 | -- 203 | -- - `hosts` (optional) containing hostnames, ports and weights. If omitted, 204 | -- ports and weights default respectively to 80 and 10. 205 | -- @param opts table with options 206 | -- @return new balancer object or nil+error 207 | -- @usage -- hosts example 208 | -- local hosts = { 209 | -- "konghq.com", -- name only, as string 210 | -- { name = "github.com" }, -- name only, as table 211 | -- { name = "getkong.org", port = 80, weight = 25 }, -- fully specified, as table 212 | -- } 213 | function _M.new(opts) 214 | assert(type(opts) == "table", "Expected an options table, but got: "..type(opts)) 215 | if not opts.log_prefix then 216 | opts.log_prefix = "least-connections" 217 | end 218 | 219 | local self = assert(balancer_base.new(opts)) 220 | 221 | -- inject overridden methods 222 | for name, method in pairs(lc) do 223 | self[name] = method 224 | end 225 | 226 | -- inject properties 227 | self.binaryHeap = binaryHeap.minUnique() -- binaryheap tracking next up address 228 | 229 | -- add the hosts provided 230 | for _, host in ipairs(opts.hosts or EMPTY) do 231 | if type(host) ~= "table" then 232 | host = { name = host } 233 | end 234 | 235 | local ok, err = self:addHost(host.name, host.port, host.weight) 236 | if not ok then 237 | return ok, "Failed creating a balancer: "..tostring(err) 238 | end 239 | end 240 | 241 | ngx_log(ngx_DEBUG, self.log_prefix, "balancer created") 242 | 243 | return self 244 | end 245 | 246 | return _M 247 | -------------------------------------------------------------------------------- /src/resty/dns/balancer/round_robin.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------- 2 | -- Round-Robin balancer 3 | -- 4 | -- @author Vinicius Mignot 5 | -- @copyright 2021 Kong Inc. All rights reserved. 6 | -- @license Apache 2.0 7 | 8 | 9 | local balancer_base = require "resty.dns.balancer.base" 10 | 11 | local ngx_log = ngx.log 12 | local ngx_DEBUG = ngx.DEBUG 13 | local random = math.random 14 | 15 | local MAX_WHEEL_SIZE = 2^32 16 | 17 | 18 | local _M = {} 19 | local roundrobin_balancer = {} 20 | 21 | 22 | -- calculate the greater common divisor, used to find the smallest wheel 23 | -- possible 24 | local function gcd(a, b) 25 | if b == 0 then 26 | return a 27 | end 28 | 29 | return gcd(b, a % b) 30 | end 31 | 32 | 33 | local function wheel_shuffle(wheel) 34 | for i = #wheel, 2, -1 do 35 | local j = random(i) 36 | wheel[i], wheel[j] = wheel[j], wheel[i] 37 | end 38 | return wheel 39 | end 40 | 41 | 42 | function roundrobin_balancer:afterHostUpdate(host) 43 | local new_wheel = {} 44 | local total_points = 0 45 | local total_weight = 0 46 | local addr_count = 0 47 | local divisor = 0 48 | 49 | -- calculate the gcd to find the proportional weight of each address 50 | for _, host in ipairs(self.hosts) do 51 | for _, address in ipairs(host.addresses) do 52 | addr_count = addr_count + 1 53 | local address_weight = address.weight 54 | divisor = gcd(divisor, address_weight) 55 | total_weight = total_weight + address_weight 56 | end 57 | end 58 | 59 | if total_weight == 0 then 60 | ngx_log(ngx_DEBUG, self.log_prefix, "trying to set a round-robin balancer with no addresses") 61 | return 62 | end 63 | 64 | if divisor > 0 then 65 | total_points = total_weight / divisor 66 | end 67 | 68 | -- add all addresses to the wheel 69 | for _, host in ipairs(self.hosts) do 70 | for _, address in ipairs(host.addresses) do 71 | local address_points = address.weight / divisor 72 | for _ = 1, address_points do 73 | new_wheel[#new_wheel + 1] = address 74 | end 75 | end 76 | end 77 | 78 | -- store the shuffled wheel 79 | self.wheel = wheel_shuffle(new_wheel) 80 | self.wheelSize = total_points 81 | self.weight = total_weight 82 | 83 | end 84 | 85 | 86 | function roundrobin_balancer:getPeer(cacheOnly, handle, hashValue) 87 | if not self.healthy then 88 | return nil, balancer_base.errors.ERR_BALANCER_UNHEALTHY 89 | end 90 | 91 | if handle then 92 | -- existing handle, so it's a retry 93 | handle.retryCount = handle.retryCount + 1 94 | else 95 | -- no handle, so this is a first try 96 | handle = self:getHandle() -- no GC specific handler needed 97 | handle.retryCount = 0 98 | end 99 | 100 | local starting_pointer = self.pointer 101 | local address 102 | local ip, port, hostname 103 | repeat 104 | self.pointer = self.pointer + 1 105 | 106 | if self.pointer > self.wheelSize then 107 | self.pointer = 1 108 | end 109 | 110 | address = self.wheel[self.pointer] 111 | if address ~= nil and address.available and not address.disabled then 112 | ip, port, hostname = address:getPeer(cacheOnly) 113 | if ip then 114 | -- success, update handle 115 | handle.address = address 116 | return ip, port, hostname, handle 117 | 118 | elseif port == balancer_base.errors.ERR_DNS_UPDATED then 119 | -- if healty we just need to try again 120 | if not self.healthy then 121 | return nil, balancer_base.errors.ERR_BALANCER_UNHEALTHY 122 | end 123 | elseif port == balancer_base.errors.ERR_ADDRESS_UNAVAILABLE then 124 | ngx_log(ngx_DEBUG, self.log_prefix, "found address but it was unavailable. ", 125 | " trying next one.") 126 | else 127 | -- an unknown error occured 128 | return nil, port 129 | end 130 | 131 | end 132 | 133 | until self.pointer == starting_pointer 134 | 135 | return nil, balancer_base.errors.ERR_NO_PEERS_AVAILABLE 136 | end 137 | 138 | 139 | function _M.new(opts) 140 | assert(type(opts) == "table", "Expected an options table, but got: "..type(opts)) 141 | if not opts.log_prefix then 142 | opts.log_prefix = "round-robin" 143 | end 144 | 145 | local self = assert(balancer_base.new(opts)) 146 | 147 | for name, method in pairs(roundrobin_balancer) do 148 | self[name] = method 149 | end 150 | 151 | -- inject additional properties 152 | self.pointer = 1 -- pointer to next-up index for the round robin scheme 153 | self.wheelSize = 0 154 | self.maxWheelSize = opts.maxWheelSize or opts.wheelSize or MAX_WHEEL_SIZE 155 | self.wheel = {} 156 | 157 | for _, host in ipairs(opts.hosts or {}) do 158 | local new_host = type(host) == "table" and host or { name = host } 159 | local ok, err = self:addHost(new_host.name, new_host.port, new_host.weight) 160 | if not ok then 161 | return ok, "Failed creating a balancer: " .. tostring(err) 162 | end 163 | end 164 | 165 | ngx_log(ngx_DEBUG, self.log_prefix, "round_robin balancer created") 166 | 167 | return self 168 | 169 | end 170 | 171 | return _M 172 | -------------------------------------------------------------------------------- /src/resty/dns/utils.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------- 2 | -- DNS utility module. 3 | -- 4 | -- Parses the `/etc/hosts` and `/etc/resolv.conf` configuration files, caches them, 5 | -- and provides some utility functions. 6 | -- 7 | -- _NOTE_: parsing the files is done using blocking i/o file operations. 8 | -- 9 | -- @copyright 2016-2020 Kong Inc. 10 | -- @author Thijs Schreijer 11 | -- @license Apache 2.0 12 | 13 | 14 | local _M = {} 15 | local utils = require("pl.utils") 16 | local gsub = string.gsub 17 | local tinsert = table.insert 18 | local time = ngx.now 19 | 20 | -- pattern that will only match data before a # or ; comment 21 | -- returns nil if there is none before the # or ; 22 | -- 2nd capture is the comment after the # or ; 23 | local PATT_COMMENT = "^([^#;]+)[#;]*(.*)$" 24 | -- Splits a string in IP and hostnames part, drops leading/trailing whitespace 25 | local PATT_IP_HOST = "^%s*([%[%]%x%.%:]+)%s+(%S.-%S)%s*$" 26 | 27 | local _DEFAULT_HOSTS = "/etc/hosts" -- hosts filename to use when omitted 28 | local _DEFAULT_RESOLV_CONF = "/etc/resolv.conf" -- resolv.conf default filename 29 | 30 | --- Default filename to parse for the `hosts` file. 31 | -- @field DEFAULT_HOSTS Defaults to `/etc/hosts` 32 | _M.DEFAULT_HOSTS = _DEFAULT_HOSTS 33 | 34 | --- Default filename to parse for the `resolv.conf` file. 35 | -- @field DEFAULT_RESOLV_CONF Defaults to `/etc/resolv.conf` 36 | _M.DEFAULT_RESOLV_CONF = _DEFAULT_RESOLV_CONF 37 | 38 | --- Maximum number of nameservers to parse from the `resolv.conf` file 39 | -- @field MAXNS Defaults to 3 40 | _M.MAXNS = 3 41 | 42 | --- Maximum number of entries to parse from `search` parameter in the `resolv.conf` file 43 | -- @field MAXSEARCH Defaults to 6 44 | _M.MAXSEARCH = 6 45 | 46 | --- Parsing configuration files and variables 47 | -- @section parsing 48 | 49 | --- Parses a `hosts` file or table. 50 | -- Does not check for correctness of ip addresses nor hostnames. Might return 51 | -- `nil + error` if the file cannot be read. 52 | -- 53 | -- __NOTE__: All output will be normalized to lowercase, IPv6 addresses will 54 | -- always be returned in brackets. 55 | -- @param filename (optional) Filename to parse, or a table with the file 56 | -- contents in lines (defaults to `'/etc/hosts'` if omitted) 57 | -- @return 1; reverse lookup table, ip addresses (table with `ipv4` and `ipv6` 58 | -- fields) indexed by their canonical names and aliases 59 | -- @return 2; list with all entries. Containing fields `ip`, `canonical` and `family`, 60 | -- and a list of aliasses 61 | -- @usage local lookup, list = utils.parseHosts({ 62 | -- "127.0.0.1 localhost", 63 | -- "1.2.3.4 someserver", 64 | -- "192.168.1.2 test.computer.com", 65 | -- "192.168.1.3 ftp.COMPUTER.com alias1 alias2", 66 | -- }) 67 | -- 68 | -- print(lookup["localhost"]) --> "127.0.0.1" 69 | -- print(lookup["ftp.computer.com"]) --> "192.168.1.3" note: name in lowercase! 70 | -- print(lookup["alias1"]) --> "192.168.1.3" 71 | _M.parseHosts = function(filename) 72 | local lines 73 | if type(filename) == "table" then 74 | lines = filename 75 | else 76 | local err 77 | lines, err = utils.readlines(filename or _M.DEFAULT_HOSTS) 78 | if not lines then return lines, err end 79 | end 80 | local result = {} 81 | local reverse = {} 82 | for _, line in ipairs(lines) do 83 | line = line:lower() 84 | local data, _ = line:match(PATT_COMMENT) 85 | if data then 86 | local ip, hosts, family, name, _ 87 | -- parse the line 88 | ip, hosts = data:match(PATT_IP_HOST) 89 | -- parse and validate the ip address 90 | if ip then 91 | name, _, family = _M.parseHostname(ip) 92 | if family ~= "ipv4" and family ~= "ipv6" then 93 | ip = nil -- not a valid IP address 94 | else 95 | ip = name 96 | end 97 | end 98 | -- add the names 99 | if ip and hosts then 100 | local entry = { ip = ip, family = family } 101 | local key = "canonical" 102 | for host in hosts:gmatch("%S+") do 103 | entry[key] = host 104 | key = (tonumber(key) or 0) + 1 105 | local rev = reverse[host] 106 | if not rev then 107 | rev = {} 108 | reverse[host] = rev 109 | end 110 | rev[family] = rev[family] or ip -- do not overwrite, first one wins 111 | end 112 | tinsert(result, entry) 113 | end 114 | end 115 | end 116 | return reverse, result 117 | end 118 | 119 | 120 | local boolOptions = { "debug", "rotate", "no-check-names", "inet6", 121 | "ip6-bytestring", "ip6-dotint", "no-ip6-dotint", 122 | "edns0", "single-request", "single-request-reopen", 123 | "no-tld-query", "use-vc"} 124 | for i, name in ipairs(boolOptions) do boolOptions[name] = name boolOptions[i] = nil end 125 | 126 | local numOptions = { "ndots", "timeout", "attempts" } 127 | for i, name in ipairs(numOptions) do numOptions[name] = name numOptions[i] = nil end 128 | 129 | -- Parses a single option. 130 | -- @param target table in which to insert the option 131 | -- @param details string containing the option details 132 | -- @return modified target table 133 | local parseOption = function(target, details) 134 | local option, n = details:match("^([^:]+)%:*(%d*)$") 135 | if boolOptions[option] and n == "" then 136 | target[option] = true 137 | if option == "ip6-dotint" then target["no-ip6-dotint"] = nil end 138 | if option == "no-ip6-dotint" then target["ip6-dotint"] = nil end 139 | elseif numOptions[option] and tonumber(n) then 140 | target[option] = tonumber(n) 141 | end 142 | end 143 | 144 | --- Parses a `resolv.conf` file or table. 145 | -- Does not check for correctness of ip addresses nor hostnames, bad options 146 | -- will be ignored. Might return `nil + error` if the file cannot be read. 147 | -- @param filename (optional) File to parse (defaults to `'/etc/resolv.conf'` if 148 | -- omitted) or a table with the file contents in lines. 149 | -- @return a table with fields `nameserver` (table), `domain` (string), `search` (table), 150 | -- `sortlist` (table) and `options` (table) 151 | -- @see applyEnv 152 | _M.parseResolvConf = function(filename) 153 | local lines 154 | if type(filename) == "table" then 155 | lines = filename 156 | else 157 | local err 158 | lines, err = utils.readlines(filename or _M.DEFAULT_RESOLV_CONF) 159 | if not lines then return lines, err end 160 | end 161 | local result = {} 162 | for _,line in ipairs(lines) do 163 | local data, _ = line:match(PATT_COMMENT) 164 | if data then 165 | local option, details = data:match("^%s*(%a+)%s+(.-)%s*$") 166 | if option == "nameserver" then 167 | result.nameserver = result.nameserver or {} 168 | if #result.nameserver < _M.MAXNS then 169 | tinsert(result.nameserver, details:lower()) 170 | end 171 | elseif option == "domain" then 172 | result.search = nil -- mutually exclusive, last one wins 173 | result.domain = details:lower() 174 | elseif option == "search" then 175 | result.domain = nil -- mutually exclusive, last one wins 176 | local search = {} 177 | result.search = search 178 | for host in details:gmatch("%S+") do 179 | if #search < _M.MAXSEARCH then 180 | tinsert(search, host:lower()) 181 | end 182 | end 183 | elseif option == "sortlist" then 184 | local list = {} 185 | result.sortlist = list 186 | for ips in details:gmatch("%S+") do 187 | tinsert(list, ips) 188 | end 189 | elseif option == "options" then 190 | result.options = result.options or {} 191 | parseOption(result.options, details) 192 | end 193 | end 194 | end 195 | return result 196 | end 197 | 198 | --- Will parse `LOCALDOMAIN` and `RES_OPTIONS` environment variables. 199 | -- It will insert them into the given `resolv.conf` based configuration table. 200 | -- 201 | -- __NOTE__: if the input is `nil+error` it will return the input, to allow for 202 | -- pass-through error handling 203 | -- @param config Options table, as parsed by `parseResolvConf`, or an empty table to get only the environment options 204 | -- @return modified table 205 | -- @see parseResolvConf 206 | -- @usage -- errors are passed through, so this; 207 | -- local config, err = utils.parseResolvConf() 208 | -- if config then 209 | -- config, err = utils.applyEnv(config) 210 | -- end 211 | -- 212 | -- -- Is identical to; 213 | -- local config, err = utils.applyEnv(utils.parseResolvConf()) 214 | _M.applyEnv = function(config, err) 215 | if not config then return config, err end -- allow for 'nil+error' pass-through 216 | local localdomain = os.getenv("LOCALDOMAIN") or "" 217 | if localdomain ~= "" then 218 | config.domain = nil -- mutually exclusive, last one wins 219 | local search = {} 220 | config.search = search 221 | for host in localdomain:gmatch("%S+") do 222 | tinsert(search, host:lower()) 223 | end 224 | end 225 | 226 | local options = os.getenv("RES_OPTIONS") or "" 227 | if options ~= "" then 228 | config.options = config.options or {} 229 | for option in options:gmatch("%S+") do 230 | parseOption(config.options, option) 231 | end 232 | end 233 | return config 234 | end 235 | 236 | --- Caching configuration files and variables 237 | -- @section caching 238 | 239 | -- local caches 240 | local cacheHosts -- cached value 241 | local cacheHostsr -- cached value 242 | local lastHosts = 0 -- timestamp 243 | local ttlHosts -- time to live for cache 244 | 245 | --- returns the `parseHosts` results, but cached. 246 | -- Once `ttl` has been provided, only after it expires the file will be parsed again. 247 | -- 248 | -- __NOTE__: if cached, the _SAME_ tables will be returned, so do not modify them 249 | -- unless you know what you are doing! 250 | -- @param ttl cache time-to-live in seconds (can be updated in following calls) 251 | -- @return reverse and list tables, same as `parseHosts`. 252 | -- @see parseHosts 253 | _M.getHosts = function(ttl) 254 | ttlHosts = ttl or ttlHosts 255 | local now = time() 256 | if (not ttlHosts) or (lastHosts + ttlHosts <= now) then 257 | cacheHosts = nil -- expired 258 | cacheHostsr = nil -- expired 259 | end 260 | 261 | if not cacheHosts then 262 | cacheHostsr, cacheHosts = _M.parseHosts() 263 | lastHosts = now 264 | end 265 | 266 | return cacheHostsr, cacheHosts 267 | end 268 | 269 | 270 | local cacheResolv -- cached value 271 | local lastResolv = 0 -- timestamp 272 | local ttlResolv -- time to live for cache 273 | 274 | --- returns the `applyEnv` results, but cached. 275 | -- Once `ttl` has been provided, only after it expires it will be parsed again. 276 | -- 277 | -- __NOTE__: if cached, the _SAME_ table will be returned, so do not modify them 278 | -- unless you know what you are doing! 279 | -- @param ttl cache time-to-live in seconds (can be updated in following calls) 280 | -- @return configuration table, same as `parseResolveConf`. 281 | -- @see parseResolvConf 282 | _M.getResolv = function(ttl) 283 | ttlResolv = ttl or ttlResolv 284 | local now = time() 285 | if (not ttlResolv) or (lastResolv + ttlResolv <= now) then 286 | cacheResolv = nil -- expired 287 | end 288 | 289 | if not cacheResolv then 290 | lastResolv = now 291 | cacheResolv = _M.applyEnv(_M.parseResolvConf()) 292 | end 293 | 294 | return cacheResolv 295 | end 296 | 297 | --- Miscellaneous 298 | -- @section miscellaneous 299 | 300 | --- checks the hostname type; ipv4, ipv6, or name. 301 | -- Type is determined by exclusion, not by validation. So if it returns `'ipv6'` then 302 | -- it can only be an ipv6, but it is not necessarily a valid ipv6 address. 303 | -- @param name the string to check (this may contain a port number) 304 | -- @return string either; `'ipv4'`, `'ipv6'`, or `'name'` 305 | -- @usage hostnameType("123.123.123.123") --> "ipv4" 306 | -- hostnameType("127.0.0.1:8080") --> "ipv4" 307 | -- hostnameType("::1") --> "ipv6" 308 | -- hostnameType("[::1]:8000") --> "ipv6" 309 | -- hostnameType("some::thing") --> "ipv6", but invalid... 310 | _M.hostnameType = function(name) 311 | local remainder, colons = gsub(name, ":", "") 312 | if colons > 1 then return "ipv6" end 313 | if remainder:match("^[%d%.]+$") then return "ipv4" end 314 | return "name" 315 | end 316 | 317 | --- parses a hostname with an optional port. 318 | -- Does not validate the name/ip. IPv6 addresses are always returned in 319 | -- square brackets, even if the input wasn't. 320 | -- @param name the string to check (this may contain a port number) 321 | -- @return `name/ip` + `port (or nil)` + `type` (one of: `"ipv4"`, `"ipv6"`, or `"name"`) 322 | _M.parseHostname = function(name) 323 | local t = _M.hostnameType(name) 324 | if t == "ipv4" or t == "name" then 325 | local ip, port = name:match("^([^:]+)%:*(%d*)$") 326 | return ip, tonumber(port), t 327 | elseif t == "ipv6" then 328 | if name:match("%[") then -- brackets, so possibly a port 329 | local ip, port = name:match("^%[([^%]]+)%]*%:*(%d*)$") 330 | return "["..ip.."]", tonumber(port), t 331 | end 332 | return "["..name.."]", nil, t -- no brackets also means no port 333 | end 334 | return nil, nil, nil -- should never happen 335 | end 336 | 337 | return _M 338 | -------------------------------------------------------------------------------- /t/00-sanity.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings FATAL => 'all'; 3 | use Test::Nginx::Socket::Lua; 4 | 5 | plan tests => 2; 6 | 7 | run_tests(); 8 | 9 | __DATA__ 10 | 11 | === TEST 1: load lua-resty-dns-client 12 | --- config 13 | location = /t { 14 | access_by_lua_block { 15 | local client = require("resty.dns.client") 16 | assert(client.init()) 17 | local host = "localhost" 18 | local typ = client.TYPE_A 19 | local answers, err = assert(client.resolve(host, { qtype = typ })) 20 | ngx.say(answers[1].address) 21 | } 22 | } 23 | --- request 24 | GET /t 25 | --- response_body 26 | 127.0.0.1 27 | --- no_error_log 28 | -------------------------------------------------------------------------------- /t/01-phases.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | plan tests => repeat_each() * (blocks() * 5); 4 | 5 | workers(6); 6 | 7 | no_shuffle(); 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: client supports access phase 13 | --- config 14 | location = /t { 15 | access_by_lua_block { 16 | local client = require("resty.dns.client") 17 | assert(client.init()) 18 | local host = "localhost" 19 | local typ = client.TYPE_A 20 | local answers, err = client.resolve(host, { qtype = typ }) 21 | 22 | if not answers then 23 | ngx.say("failed to resolve: ", err) 24 | end 25 | 26 | ngx.say("address name: ", answers[1].name) 27 | } 28 | } 29 | --- request 30 | GET /t 31 | --- response_body 32 | address name: localhost 33 | --- no_error_log 34 | [error] 35 | dns lookup pool exceeded retries 36 | API disabled in the context of init_worker_by_lua 37 | 38 | 39 | 40 | === TEST 2: client does not support init_worker phase 41 | --- http_config eval 42 | qq { 43 | init_worker_by_lua_block { 44 | local client = require("resty.dns.client") 45 | assert(client.init()) 46 | local host = "konghq.com" 47 | local typ = client.TYPE_A 48 | answers, err = client.resolve(host, { qtype = typ }) 49 | } 50 | } 51 | --- config 52 | location = /t { 53 | access_by_lua_block { 54 | ngx.say("answers: ", answers) 55 | ngx.say("err: ", err) 56 | } 57 | } 58 | --- request 59 | GET /t 60 | --- response_body 61 | answers: nil 62 | err: dns client error: 101 empty record received 63 | --- no_error_log 64 | [error] 65 | dns lookup pool exceeded retries 66 | API disabled in the context of init_worker_by_lua 67 | -------------------------------------------------------------------------------- /t/02-timer-usage.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | plan tests => repeat_each() * (blocks() * 5); 4 | 5 | workers(6); 6 | 7 | no_shuffle(); 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: reuse timers for queries of same name, independent on # of workers 13 | --- http_config eval 14 | qq { 15 | init_worker_by_lua_block { 16 | local client = require("resty.dns.client") 17 | assert(client.init({ 18 | nameservers = { "8.8.8.8" }, 19 | hosts = {}, -- empty tables to parse to prevent defaulting to /etc/hosts 20 | resolvConf = {}, -- and resolv.conf files 21 | order = { "A" }, 22 | })) 23 | local host = "httpbin.org" 24 | local typ = client.TYPE_A 25 | for i = 1, 10 do 26 | client.resolve(host, { qtype = typ }) 27 | end 28 | 29 | local host = "mockbin.org" 30 | for i = 1, 10 do 31 | client.resolve(host, { qtype = typ }) 32 | end 33 | 34 | workers = ngx.worker.count() 35 | timers = ngx.timer.pending_count() 36 | } 37 | } 38 | --- config 39 | location = /t { 40 | access_by_lua_block { 41 | local client = require("resty.dns.client") 42 | assert(client.init()) 43 | local host = "httpbin.org" 44 | local typ = client.TYPE_A 45 | local answers, err = client.resolve(host, { qtype = typ }) 46 | 47 | if not answers then 48 | ngx.say("failed to resolve: ", err) 49 | end 50 | 51 | ngx.say("first address name: ", answers[1].name) 52 | 53 | host = "mockbin.org" 54 | answers, err = client.resolve(host, { qtype = typ }) 55 | 56 | if not answers then 57 | ngx.say("failed to resolve: ", err) 58 | end 59 | 60 | ngx.say("second address name: ", answers[1].name) 61 | 62 | ngx.say("workers: ", workers) 63 | 64 | -- should be 2 timers maximum (1 for each hostname) 65 | ngx.say("timers: ", timers) 66 | } 67 | } 68 | --- request 69 | GET /t 70 | --- response_body 71 | first address name: httpbin.org 72 | second address name: mockbin.org 73 | workers: 6 74 | timers: 2 75 | --- no_error_log 76 | [error] 77 | dns lookup pool exceeded retries 78 | API disabled in the context of init_worker_by_lua 79 | --------------------------------------------------------------------------------