├── .gitignore ├── .travis.yml ├── DEPLOYING.md ├── LICENSE ├── README.md ├── README.spotify ├── catalog-info.yaml ├── circle.yml ├── pom.xml └── src ├── main └── java │ └── com │ └── spotify │ └── dns │ ├── AbstractChangeNotifier.java │ ├── AggregatingChangeNotifier.java │ ├── CachingLookupFactory.java │ ├── ChangeNotifier.java │ ├── ChangeNotifierFactory.java │ ├── ChangeNotifiers.java │ ├── DirectChangeNotifier.java │ ├── DnsException.java │ ├── DnsSrvResolver.java │ ├── DnsSrvResolvers.java │ ├── DnsSrvWatcher.java │ ├── DnsSrvWatcherFactory.java │ ├── DnsSrvWatchers.java │ ├── ErrorHandler.java │ ├── LookupFactory.java │ ├── LookupResult.java │ ├── MeteredDnsSrvResolver.java │ ├── PollingDnsSrvWatcher.java │ ├── RetainingDnsSrvResolver.java │ ├── ServiceResolvingChangeNotifier.java │ ├── SimpleLookupFactory.java │ ├── StaticChangeNotifier.java │ ├── XBillDnsSrvResolver.java │ └── statistics │ ├── DnsReporter.java │ └── DnsTimingContext.java └── test └── java └── com └── spotify └── dns ├── AbstractChangeNotifierTest.java ├── AggregatingChangeNotifierTest.java ├── CachingLookupFactoryTest.java ├── DnsLookupPerformanceTest.java ├── DnsSrvResolversIT.java ├── DnsSrvWatchersTest.java ├── DnsTestUtil.java ├── MeteredDnsSrvResolverTest.java ├── RetainingDnsSrvResolverTest.java ├── ServiceResolvingChangeNotifierTest.java ├── SimpleLookupFactoryTest.java ├── XBillDnsSrvResolverTest.java └── examples ├── BasicUsage.java └── PollingUsage.java /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *# 6 | target 7 | .idea 8 | .DS_Store 9 | .project 10 | .classpath 11 | .settings 12 | .cache 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /DEPLOYING.md: -------------------------------------------------------------------------------- 1 | # Deploying Instructions 2 | 3 | These instructions are based on the [instructions](http://central.sonatype.org/pages/ossrh-guide.html) 4 | for deploying to the Central Repository using [Maven](http://central.sonatype.org/pages/apache-maven.html). 5 | Note that this is for Spotify internal use only. 6 | 7 | You will need the following: 8 | - The username and password that Spotify uses to deploy to the Central Repository as described in 9 | the open source manual on the internal wiki. 10 | - [GPG set up on the machine you're deploying from](http://central.sonatype.org/pages/working-with-pgp-signatures.html) 11 | 12 | Once you've got that in place, you should be able to do deployment using the following commands: 13 | 14 | ``` 15 | # deploy snapshot version 16 | mvn clean deploy 17 | 18 | # make and deploy a relase 19 | mvn release:clean release:prepare -P release 20 | mvn release:perform 21 | ``` 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2012 Spotify AB 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE 2 | 3 | This repo is no longer actively maintained. While it should continue to work and there are no major known bugs, we will not be improving dns-java or releasing new versions. 4 | 5 | [![Circle CI](https://circleci.com/gh/spotify/dns-java.svg?style=svg)](https://circleci.com/gh/spotify/dns-java) 6 | [![Coverage Status](https://coveralls.io/repos/spotify/dns-java/badge.svg?branch=master&service=github)](https://coveralls.io/github/spotify/dns-java?branch=master) 7 | [![Maven Central](https://img.shields.io/maven-central/v/com.spotify/dns.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.spotify%22%20dns*) 8 | [![License](https://img.shields.io/github/license/spotify/dns-java.svg)](LICENSE) 9 | 10 | spotify-dns-java 11 | ================ 12 | 13 | This small DNS wrapper library provides some useful pieces of functionality related to SRV lookups. 14 | 15 | ## Resilience 16 | 17 | Sometimes it is useful to default to previously returned, retained values, if a dns lookup should 18 | fail or return an empty result. This behavior is controlled by the ```retainingDataOnFailures()``` 19 | and ```retentionDurationMillis(long)``` methods in 20 | [DnsSrvResolvers.DnsSrvResolverBuilder](src/main/java/com/spotify/dns/DnsSrvResolvers.java). 21 | 22 | ## Watching for Changes 23 | 24 | It's often useful to update where you try to connect based on changes in lookup results, and this library 25 | provides functionality that allows you to get notified when things change by implementing this interface (defined in the [ChangeNotifier](src/main/java/com/spotify/dns/ChangeNotifier.java) interface): 26 | 27 | ```java 28 | interface Listener { 29 | 30 | /** 31 | * Signal that set of records changed. 32 | * 33 | * @param changeNotification An object containing details about the change 34 | */ 35 | void onChange(ChangeNotification changeNotification); 36 | } 37 | 38 | /** 39 | * A change event containing the current and previous set of records. 40 | */ 41 | interface ChangeNotification { 42 | Set current(); 43 | Set previous(); 44 | } 45 | ``` 46 | 47 | Take a look at the [PollingUsage example](src/test/java/com/spotify/dns/examples/PollingUsage.java) for an example. 48 | 49 | ## Metrics 50 | 51 | If you have a statistics system that can be integrated with using the munin protocol, the method 52 | metered() in DnsSrvResolvers.DnsSrvResolverBuilder enables this in conjunction with the spotify 53 | munin forwarder. Have a look at the 54 | [BasicUsage example](src/test/java/com/spotify/dns/examples/BasicUsage.java) for details on how to 55 | set that up. 56 | 57 | ## Usage 58 | 59 | The entry point to lookups is through an instance of 60 | [DnsSrvResolver](src/main/java/com/spotify/dns/DnsSrvResolver.java) obtained via the 61 | [DnsSrvResolvers](src/main/java/com/spotify/dns/DnsSrvResolvers.java) factory class. 62 | 63 | To periodically check a set of records and react to changes, use the 64 | [DnsSrvWatcher](src/main/java/com/spotify/dns/DnsSrvWatcher.java) interface obtained via the 65 | [DnsSrvWatchers](src/main/java/com/spotify/dns/DnsSrvWatchers.java) factory class. 66 | 67 | For example code, have a look at 68 | [BasicUsage example](src/test/java/com/spotify/dns/examples/BasicUsage.java) and 69 | [PollingUsage example](src/test/java/com/spotify/dns/examples/PollingUsage.java) 70 | 71 | To include the latest released version in your maven project, do: 72 | ```xml 73 | 74 | com.spotify 75 | dns 76 | 3.2.2 77 | 78 | ``` 79 | 80 | NOTE: version 3.1.0 is broken; you cannot use the retention feature in that version. 81 | 82 | ## License 83 | 84 | This software is released under the Apache License 2.0. More information in the file LICENSE 85 | distributed with this project. 86 | -------------------------------------------------------------------------------- /README.spotify: -------------------------------------------------------------------------------- 1 | SPOTIFY PROJECT 2 | ----------------- 3 | 4 | 5 | Copyright (c) 2015 Spotify AB 6 | 7 | THIS REPOSITORY IS THE PROPERTY OF SPOTIFY AB 8 | UNAUTHORIZED ACCESS TO THIS DATA IS STRICTLY PROHIBITED. 9 | 10 | README Template Version 5 11 | 12 | 13 | Project : java/dns 14 | 15 | Maintainer : Tools Squad 16 | 17 | Dependencies: 18 | 19 | Environment : 20 | 21 | Categories : [x] Infrastructure 22 | [ ] Database Maintenance 23 | [ ] Shared Library 24 | [ ] Media Data Processing 25 | [ ] Meta Data Processing 26 | [ ] Deep Server 27 | [ ] Shallow Server 28 | [ ] Client 29 | 30 | 31 | USAGE 32 | ------ 33 | 34 | DNS SRV record lookup. 35 | 36 | DESCRIPTION 37 | ------------ 38 | 39 | BUILD 40 | ------ 41 | 42 | mvn compile 43 | 44 | INSTALLATION 45 | ------------- 46 | 47 | # Installation to your local maven repository: 48 | 49 | mvn install 50 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: dns-java 5 | spec: 6 | type: library 7 | owner: fabric 8 | 9 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | _JAVA_OPTIONS: "-Xms512m -Xmx2024m" 4 | 5 | test: 6 | override: 7 | - mvn -Pcoverage verify 8 | post: 9 | - mvn org.eluder.coveralls:coveralls-maven-plugin:report -Dcoveralls.token=$COVERALLS_TOKEN 10 | - mkdir -p $CIRCLE_TEST_REPORTS/junit/ 11 | - find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \; 12 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.spotify 5 | dns 6 | bundle 7 | 3.3.3-SNAPSHOT 8 | Spotify DNS wrapper library 9 | A thin wrapper around dnsjava for some features related to SRV lookups. 10 | 11 | https://github.com/spotify/dns-java 12 | 13 | 14 | UTF-8 15 | 30.1.1-jre 16 | 8 17 | 8 18 | 19 | 20 | 21 | git@github.com:spotify/dns-java.git 22 | scm:git:git@github.com:spotify/dns-java.git 23 | scm:git:git@github.com:spotify/dns-java.git 24 | HEAD 25 | 26 | 27 | 28 | 29 | The Apache Software License, Version 2.0 30 | http://www.apache.org/licenses/LICENSE-2.0.txt 31 | repo 32 | 33 | 34 | 35 | 36 | 37 | petter 38 | petter@spotify.com 39 | Petter Måhlén 40 | 41 | 42 | rouz 43 | Rouzbeh Delavari 44 | rouz@spotify.com 45 | 46 | 47 | 48 | 49 | 50 | dnsjava 51 | dnsjava 52 | 3.5.2 53 | 54 | 55 | com.google.guava 56 | guava 57 | ${guava.version} 58 | 59 | 60 | org.slf4j 61 | slf4j-api 62 | 1.7.36 63 | 64 | 65 | junit 66 | junit 67 | 4.13.1 68 | test 69 | 70 | 71 | org.mockito 72 | mockito-core 73 | 1.9.5 74 | test 75 | 76 | 77 | org.hamcrest 78 | hamcrest-library 79 | 1.3 80 | test 81 | 82 | 83 | com.google.guava 84 | guava-testlib 85 | ${guava.version} 86 | test 87 | 88 | 89 | org.slf4j 90 | slf4j-simple 91 | 1.7.30 92 | test 93 | 94 | 95 | 96 | com.jayway.awaitility 97 | awaitility 98 | 1.7.0 99 | test 100 | 101 | 102 | 103 | 104 | org.objenesis 105 | objenesis 106 | 2.1 107 | test 108 | 109 | 110 | 111 | 112 | 113 | ossrh 114 | https://oss.sonatype.org/content/repositories/snapshots 115 | 116 | 117 | 118 | 119 | 120 | coverage 121 | 122 | 123 | 124 | org.jacoco 125 | jacoco-maven-plugin 126 | 127 | 128 | **/AutoValue_* 129 | 130 | 131 | 132 | 133 | 134 | prepare-agent 135 | 136 | 137 | 138 | report 139 | prepare-package 140 | 141 | report 142 | 143 | 144 | 145 | check 146 | 147 | check 148 | 149 | 150 | 151 | 152 | BUNDLE 153 | 154 | 155 | INSTRUCTION 156 | COVEREDRATIO 157 | 0.61 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | release 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-gpg-plugin 176 | 1.5 177 | 178 | 179 | sign-artifacts 180 | verify 181 | 182 | sign 183 | 184 | 185 | 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-source-plugin 190 | 2.2.1 191 | 192 | 193 | attach-sources 194 | 195 | jar-no-fork 196 | 197 | 198 | 199 | 200 | 201 | org.apache.maven.plugins 202 | maven-javadoc-plugin 203 | 204 | 205 | attach-javadocs 206 | 207 | jar 208 | 209 | 210 | 211 | 212 | 213 | org.sonatype.plugins 214 | nexus-staging-maven-plugin 215 | 1.6.3 216 | true 217 | 218 | ossrh 219 | https://oss.sonatype.org/ 220 | true 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | org.apache.maven.plugins 233 | maven-javadoc-plugin 234 | 3.2.0 235 | 236 | 237 | 8 238 | 239 | 240 | 241 | 242 | 243 | 244 | maven-compiler-plugin 245 | 3.8.1 246 | 247 | 8 248 | 8 249 | 8 250 | 251 | 252 | 253 | maven-surefire-plugin 254 | 2.22.2 255 | 256 | 257 | --add-opens com.spotify.dns/com.spotify.dns=ALL-UNNAMED 258 | 259 | 260 | 261 | org.apache.maven.plugins 262 | maven-enforcer-plugin 263 | 1.2 264 | 265 | 266 | enforce 267 | 268 | 269 | 270 | 271 | 272 | 273 | enforce 274 | 275 | 276 | 277 | 278 | 279 | org.apache.felix 280 | maven-bundle-plugin 281 | 4.2.1 282 | true 283 | 284 | 285 | ${project.name} 286 | ${project.groupId}.${project.artifactId} 287 | 288 | com.spotify.dns;-noimport:=true, 289 | com.spotify.dns.statistics;-noimport:=true 290 | 291 | ${project.name} 292 | ${project.version} 293 | 294 | 295 | 296 | 300 | 301 | 302 | manifest 303 | 304 | 305 | 306 | 307 | 308 | com.github.siom79.japicmp 309 | japicmp-maven-plugin 310 | 0.14.3 311 | 312 | 313 | 314 | com.spotify 315 | ${project.artifactId} 316 | 3.3.2 317 | 318 | 319 | 320 | 321 | ${project.build.directory}/${project.artifactId}-${project.version}.jar 322 | 323 | 324 | 325 | true 326 | true 327 | 328 | 329 | 330 | 331 | verify 332 | 333 | cmp 334 | 335 | 336 | 337 | 338 | 339 | org.eluder.coveralls 340 | coveralls-maven-plugin 341 | 4.1.0 342 | 343 | ${coveralls.token} 344 | 345 | 346 | 347 | org.apache.maven.plugins 348 | maven-release-plugin 349 | 2.5 350 | 351 | v@{project.version} 352 | true 353 | false 354 | release 355 | deploy 356 | 357 | 358 | 359 | org.sonatype.plugins 360 | nexus-staging-maven-plugin 361 | 1.6.3 362 | true 363 | 364 | ossrh 365 | https://oss.sonatype.org/ 366 | true 367 | 368 | 369 | 370 | 371 | 372 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/AbstractChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import com.google.common.collect.ImmutableSet; 22 | import com.google.common.collect.Sets; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.util.Collections; 27 | import java.util.Set; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | import java.util.concurrent.atomic.AtomicReference; 30 | import java.util.concurrent.locks.ReentrantLock; 31 | 32 | /** 33 | * A helper for implementing the {@link ChangeNotifier} interface. 34 | */ 35 | abstract class AbstractChangeNotifier implements ChangeNotifier { 36 | 37 | private static final Logger log = LoggerFactory.getLogger(AbstractChangeNotifier.class); 38 | 39 | private final AtomicReference> listenerRef = new AtomicReference<>(); 40 | 41 | private final AtomicBoolean listenerNotified = new AtomicBoolean(false); 42 | 43 | private final ReentrantLock lock = new ReentrantLock(); 44 | 45 | @Override 46 | public void setListener(final Listener listener, final boolean fire) { 47 | requireNonNull(listener, "listener"); 48 | 49 | lock.lock(); 50 | try { 51 | if (!listenerRef.compareAndSet(null, listener)) { 52 | throw new IllegalStateException("Listener already set!"); 53 | } 54 | 55 | if (fire) { 56 | notifyListener(newChangeNotification(current(), Sets.newHashSet()), true); 57 | } 58 | } finally { 59 | lock.unlock(); 60 | } 61 | } 62 | 63 | @Override 64 | public final void close() { 65 | listenerRef.set(null); 66 | closeImplementation(); 67 | } 68 | 69 | protected abstract void closeImplementation(); 70 | 71 | protected final void fireRecordsUpdated(ChangeNotification changeNotification) { 72 | notifyListener(changeNotification, false); 73 | } 74 | 75 | /** 76 | * Notify the listener about a change. If this is due to adding a new listener rather than 77 | * being an update, only notify the listener if this is the first notification sent to it. 78 | * 79 | * @param changeNotification the change notification to send 80 | * @param newListener call is triggered by adding a listener rather than an update 81 | */ 82 | private void notifyListener(ChangeNotification changeNotification, boolean newListener) { 83 | lock.lock(); 84 | try { 85 | requireNonNull(changeNotification, "changeNotification"); 86 | 87 | final Listener listener = listenerRef.get(); 88 | if (listener != null) { 89 | try { 90 | final boolean notified = listenerNotified.getAndSet(true); 91 | if (!(newListener && notified)) { 92 | listener.onChange(changeNotification); 93 | } 94 | } catch (Throwable e) { 95 | log.error("Change notification listener threw exception", e); 96 | } 97 | } 98 | } finally { 99 | lock.unlock(); 100 | } 101 | } 102 | 103 | protected final ChangeNotification newChangeNotification(Set current, Set previous) { 104 | requireNonNull(current, "current"); 105 | requireNonNull(previous, "previous"); 106 | 107 | return new ChangeNotificationImpl<>(current, previous); 108 | } 109 | 110 | private static class ChangeNotificationImpl implements ChangeNotification { 111 | 112 | private final Set current; 113 | private final Set previous; 114 | 115 | protected ChangeNotificationImpl(Set current, Set previous) { 116 | this.current = current; 117 | this.previous = previous; 118 | } 119 | 120 | @Override 121 | public Set current() { 122 | return unmodifiable(current); 123 | } 124 | 125 | private Set unmodifiable(Set set) { 126 | if (ChangeNotifiers.isInitialEmptyData(set)) { 127 | return set; 128 | } 129 | if (set instanceof ImmutableSet) { 130 | return set; 131 | } 132 | return Collections.unmodifiableSet(set); 133 | } 134 | 135 | @Override 136 | public Set previous() { 137 | return unmodifiable(previous); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/AggregatingChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import com.google.common.collect.ImmutableList; 20 | import com.google.common.collect.ImmutableSet; 21 | 22 | import java.util.List; 23 | import java.util.Set; 24 | 25 | /** 26 | * A {@link ChangeNotifier} that aggregates the records provided by a list of notifiers. 27 | */ 28 | class AggregatingChangeNotifier extends AbstractChangeNotifier { 29 | 30 | private final List> changeNotifiers; 31 | 32 | private volatile Set records; 33 | 34 | /** 35 | * Create a new aggregating {@link ChangeNotifier}. 36 | * 37 | * @param changeNotifiers the notifiers to aggregate 38 | */ 39 | AggregatingChangeNotifier(final Iterable> changeNotifiers) { 40 | this.changeNotifiers = ImmutableList.copyOf(changeNotifiers); 41 | 42 | // Set up forwarding of listeners 43 | for (final ChangeNotifier changeNotifier : this.changeNotifiers) { 44 | changeNotifier.setListener(ignored -> checkChange(), false); 45 | } 46 | 47 | records = aggregateSet(); 48 | } 49 | 50 | @Override 51 | public Set current() { 52 | return records; 53 | } 54 | 55 | @Override 56 | protected void closeImplementation() { 57 | for (ChangeNotifier provider : changeNotifiers) { 58 | provider.close(); 59 | } 60 | } 61 | 62 | private synchronized void checkChange() { 63 | Set currentRecords = aggregateSet(); 64 | 65 | if (ChangeNotifiers.isNoLongerInitial(currentRecords, records) || !currentRecords.equals(records)) { 66 | final ChangeNotification changeNotification = 67 | newChangeNotification(currentRecords, records); 68 | records = currentRecords; 69 | 70 | fireRecordsUpdated(changeNotification); 71 | } 72 | } 73 | 74 | private Set aggregateSet() { 75 | if (areAllInitial(changeNotifiers)) { 76 | return ChangeNotifiers.initialEmptyDataInstance(); 77 | } 78 | 79 | ImmutableSet.Builder records = ImmutableSet.builder(); 80 | for (final ChangeNotifier changeNotifier : changeNotifiers) { 81 | records.addAll(changeNotifier.current()); 82 | } 83 | return records.build(); 84 | } 85 | 86 | private boolean areAllInitial(List> changeNotifiers) { 87 | for (final ChangeNotifier changeNotifier : changeNotifiers) { 88 | if (!ChangeNotifiers.isInitialEmptyData(changeNotifier.current())) { 89 | return false; 90 | } 91 | } 92 | return true; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/CachingLookupFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import com.google.common.cache.Cache; 22 | import com.google.common.cache.CacheBuilder; 23 | import com.google.common.util.concurrent.UncheckedExecutionException; 24 | import java.util.concurrent.ExecutionException; 25 | import org.xbill.DNS.Lookup; 26 | import org.xbill.DNS.lookup.LookupSession; 27 | 28 | /** 29 | * Caches Lookup instances using a per-thread cache; this is so that different threads will never 30 | * get the same instance of Lookup. Lookup instances are not thread-safe. 31 | */ 32 | @Deprecated 33 | class CachingLookupFactory implements LookupFactory { 34 | private final LookupFactory delegate; 35 | private final ThreadLocal> cacheHolder; 36 | 37 | CachingLookupFactory(LookupFactory delegate) { 38 | this.delegate = requireNonNull(delegate, "delegate"); 39 | cacheHolder = 40 | ThreadLocal.withInitial(() -> CacheBuilder.newBuilder().build()); 41 | } 42 | 43 | @Override 44 | public Lookup forName(final String fqdn) { 45 | try { 46 | return cacheHolder.get().get( 47 | fqdn, 48 | () -> delegate.forName(fqdn) 49 | ); 50 | } catch (ExecutionException e) { 51 | throw new DnsException(e); 52 | } catch (UncheckedExecutionException e) { 53 | throw new DnsException(e); 54 | } 55 | } 56 | 57 | @Override 58 | public LookupSession sessionForName(String fqdn) { 59 | throw new java.lang.UnsupportedOperationException("Session not supported with caching lookup"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/ChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import java.util.Set; 20 | 21 | /** 22 | * A change notifier represents a watched lookup from a {@link DnsSrvWatcher}. 23 | * 24 | *

The records can be of any type. Usually something that directly reflects what your 25 | * application will use the records for. 26 | * 27 | *

A {@link Listener} can be attached to listen to change events on the watched set of records. 28 | * 29 | * @param The records type 30 | */ 31 | public interface ChangeNotifier { 32 | 33 | /** 34 | * Get the current set of records. 35 | * 36 | * @return The current set of records 37 | */ 38 | Set current(); 39 | 40 | /** 41 | * Set a listener to be called when the set of records change. 42 | * 43 | *

One one listener can be added. Multiple calls to this method is an error. 44 | * 45 | * @param listener The listener to set 46 | * @param fire Fire the notification event immediately. Can be used to ensure that no updates 47 | * are missed when setting the listener 48 | * @throws IllegalStateException if called more than once 49 | */ 50 | void setListener(Listener listener, boolean fire); 51 | 52 | /** 53 | * Close this {@link ChangeNotifier}, releasing any resources allocated. Once closed, no more 54 | * {@link Listener} events will be fired. Implementations of {@link ChangeNotifier} are not 55 | * allowed to throw checked exceptions from close(). 56 | */ 57 | void close(); 58 | 59 | /** 60 | * A listener which will be called when the set of records change 61 | */ 62 | @FunctionalInterface 63 | interface Listener { 64 | 65 | /** 66 | * Signal that set of records changed. 67 | * 68 | * @param changeNotification An object containing details about the change 69 | */ 70 | void onChange(ChangeNotification changeNotification); 71 | } 72 | 73 | /** 74 | * A change event containing the current and previous set of records. 75 | */ 76 | interface ChangeNotification { 77 | Set current(); 78 | Set previous(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/ChangeNotifierFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | /** 20 | * Creates a {@link RunnableChangeNotifier} from a FQDN. 21 | * 22 | *

Intended to be used from {@link DnsSrvWatcherFactory} when implementing custom triggering 23 | * schemes for {@link DnsSrvWatcher}s. 24 | */ 25 | public interface ChangeNotifierFactory { 26 | 27 | /** 28 | * Creates a {@link ChangeNotifier} that is a {@link Runnable}. When a check for a change should 29 | * be executed is up to the caller of this method. 30 | * 31 | * @param fqdn The FQDN for the change notifier 32 | * @return A runnable change notifier 33 | */ 34 | RunnableChangeNotifier create(String fqdn); 35 | 36 | /** 37 | * A {@link ChangeNotifier} that that can be executed. A call to {@link Runnable#run()} will 38 | * trigger a check for record changes. 39 | */ 40 | interface RunnableChangeNotifier extends ChangeNotifier, Runnable { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/ChangeNotifiers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import com.google.common.collect.Sets; 20 | 21 | import static com.spotify.dns.ChangeNotifierFactory.RunnableChangeNotifier; 22 | import static java.util.Objects.requireNonNull; 23 | 24 | import java.util.Arrays; 25 | import java.util.Collections; 26 | import java.util.HashSet; 27 | import java.util.Set; 28 | import java.util.concurrent.atomic.AtomicReference; 29 | import java.util.function.Supplier; 30 | 31 | public final class ChangeNotifiers { 32 | 33 | /** 34 | * Ensure that we get a unique empty set that you can't create any other way 35 | * (i.e. ImmutableSet.of() always returns the same instance). 36 | * 37 | * This is needed to distinguishing the initial state of change notifiers from 38 | * when they have gotten proper data. 39 | */ 40 | private static final Set INITIAL_EMPTY_DATA = Collections.unmodifiableSet(new HashSet<>()); 41 | 42 | private ChangeNotifiers() { 43 | } 44 | 45 | /** 46 | * Use this to determine if the data you get back from a notifier is the initial result of the result of a proper 47 | * DNS lookup. This is useful for distinguishing a proper but empty DNS result from the case 48 | * where a lookup has not completed yet. 49 | * @param set 50 | * @return true if the input is an initially empty set. 51 | */ 52 | public static boolean isInitialEmptyData(Set set) { 53 | return set == INITIAL_EMPTY_DATA; 54 | } 55 | 56 | static Set initialEmptyDataInstance() { 57 | return INITIAL_EMPTY_DATA; 58 | } 59 | 60 | static boolean isNoLongerInitial(Set current, Set previous) { 61 | return isInitialEmptyData(previous) && !isInitialEmptyData(current); 62 | } 63 | 64 | /** 65 | * Creates a {@link ChangeNotifier} that aggregates the records provided by a list of notifiers. 66 | * 67 | *

A change event on any of the input notifiers will propagate up the the returned notifier. 68 | * The set of previous and current records contained in the event will be the union of all 69 | * records in the input notifiers, before and after the change event. 70 | * 71 | * @param notifiers A list of notifiers to aggregate 72 | * @param The record type 73 | * @return A notifier with the described behaviour 74 | */ 75 | public static ChangeNotifier aggregate(ChangeNotifier... notifiers) { 76 | return aggregate(Arrays.asList(notifiers)); 77 | } 78 | 79 | public static ChangeNotifier aggregate(Iterable> notifiers) { 80 | return new AggregatingChangeNotifier<>(notifiers); 81 | } 82 | 83 | /** 84 | * Create a {@link ChangeNotifier} with a static set of records. 85 | * 86 | *

This notifier will never generate any change events. Thus any attached 87 | * {@link ChangeNotifier.Listener} will at most get one initial call to 88 | * {@link ChangeNotifier.Listener#onChange(ChangeNotifier.ChangeNotification)} 89 | * if they are attached with the {@code fire} argument set to {@code true}. 90 | * 91 | * @param records The records that the notifier will contain 92 | * @param The record type 93 | * @return A notifier with a static set of records 94 | */ 95 | public static ChangeNotifier staticRecords(T... records) { 96 | return staticRecords(Sets.newHashSet(records)); 97 | } 98 | 99 | public static ChangeNotifier staticRecords(Set records) { 100 | return new StaticChangeNotifier<>(records); 101 | } 102 | 103 | /** 104 | * Create a {@link RunnableChangeNotifier} that directly wraps a set of records given by a 105 | * {@link Supplier}. 106 | * 107 | *

Each call to {@link Runnable#run()} will cause the supplier to be polled and regular 108 | * change notifications to be triggered. 109 | * 110 | *

This implementation is useful for testing components that depend on a 111 | * {@link ChangeNotifier}. 112 | * 113 | * @param recordsSupplier The supplier of records 114 | * @param The record type 115 | * @return A runnable notifier 116 | */ 117 | public static RunnableChangeNotifier direct(Supplier> recordsSupplier) { 118 | return new DirectChangeNotifier<>(recordsSupplier); 119 | } 120 | 121 | /** 122 | * @deprecated Use {@link #direct(java.util.function.Supplier)} 123 | * deprecated since version 3.2.0 124 | */ 125 | public static RunnableChangeNotifier direct(com.google.common.base.Supplier> recordsSupplier) { 126 | return new DirectChangeNotifier<>(recordsSupplier); 127 | } 128 | 129 | public static RunnableChangeNotifier direct(AtomicReference> recordsHolder) { 130 | return new DirectChangeNotifier<>(supplierFromRef(recordsHolder)); 131 | } 132 | 133 | private static Supplier> supplierFromRef(final AtomicReference> ref) { 134 | requireNonNull(ref, "ref"); 135 | return ref::get; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DirectChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import java.util.Set; 22 | import java.util.function.Supplier; 23 | 24 | class DirectChangeNotifier extends AbstractChangeNotifier 25 | implements ChangeNotifierFactory.RunnableChangeNotifier { 26 | 27 | private final Supplier> recordsSupplier; 28 | 29 | private volatile Set records = ChangeNotifiers.initialEmptyDataInstance(); 30 | private volatile boolean run = true; 31 | 32 | public DirectChangeNotifier(Supplier> recordsSupplier) { 33 | this.recordsSupplier = requireNonNull(recordsSupplier, "recordsSupplier"); 34 | } 35 | 36 | @Override 37 | protected void closeImplementation() { 38 | run = false; 39 | } 40 | 41 | @Override 42 | public Set current() { 43 | return records; 44 | } 45 | 46 | @Override 47 | public void run() { 48 | if (!run) { 49 | return; 50 | } 51 | 52 | final Set current = recordsSupplier.get(); 53 | if (ChangeNotifiers.isNoLongerInitial(current, records) || !current.equals(records)) { 54 | final ChangeNotification changeNotification = 55 | newChangeNotification(current, records); 56 | records = current; 57 | 58 | fireRecordsUpdated(changeNotification); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | /** 20 | * RuntimeException thrown by the Spotify DNS library. 21 | */ 22 | public class DnsException extends RuntimeException { 23 | public DnsException() { 24 | } 25 | 26 | public DnsException(String message) { 27 | super(message); 28 | } 29 | 30 | public DnsException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | 34 | public DnsException(Throwable cause) { 35 | super(cause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsSrvResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.CompletionStage; 21 | 22 | /** 23 | * Contract for doing SRV lookups. 24 | */ 25 | public interface DnsSrvResolver { 26 | /** 27 | * Does a DNS SRV lookup for the supplied fully qualified domain name, and returns the 28 | * matching results. 29 | * @deprecated 30 | * This method is deprecated in favor of the asynchronous version. 31 | * Use {@link DnsSrvResolver#resolveAsync(String)} instead 32 | * 33 | * @param fqdn a DNS name to query for 34 | * @return a possibly empty list of matching records 35 | * @throws DnsException if there was an error doing the DNS lookup 36 | */ 37 | @Deprecated 38 | List resolve(String fqdn); 39 | 40 | /** 41 | * Does a DNS SRV lookup for the supplied fully qualified domain name, and returns the 42 | * matching results. 43 | * 44 | * @param fqdn a DNS name to query for 45 | * @return a possibly empty list of matching records 46 | * @throws DnsException if there was an error doing the DNS lookup 47 | */ 48 | default CompletionStage> resolveAsync(String fqdn) { 49 | throw new java.lang.UnsupportedOperationException("Not implemented"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsSrvResolvers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.concurrent.TimeUnit.HOURS; 20 | import static java.util.concurrent.TimeUnit.SECONDS; 21 | 22 | import com.spotify.dns.statistics.DnsReporter; 23 | import java.net.UnknownHostException; 24 | import java.time.Duration; 25 | import java.util.List; 26 | import java.util.concurrent.Executor; 27 | import java.util.concurrent.ForkJoinPool; 28 | 29 | import org.xbill.DNS.ExtendedResolver; 30 | import org.xbill.DNS.Resolver; 31 | 32 | /** 33 | * Provides builders for configuring and instantiating {@link DnsSrvResolver}s. 34 | */ 35 | public final class DnsSrvResolvers { 36 | 37 | private static final int DEFAULT_DNS_TIMEOUT_SECONDS = 5; 38 | private static final int DEFAULT_RETENTION_DURATION_HOURS = 2; 39 | 40 | public static DnsSrvResolverBuilder newBuilder() { 41 | return new DnsSrvResolverBuilder(); 42 | } 43 | 44 | public static final class DnsSrvResolverBuilder { 45 | 46 | private final DnsReporter reporter; 47 | private final boolean retainData; 48 | private final boolean cacheLookups; 49 | private final long dnsLookupTimeoutMillis; 50 | private final long retentionDurationMillis; 51 | private final List servers; 52 | private final Executor executor; 53 | 54 | private DnsSrvResolverBuilder() { 55 | this(null, 56 | false, 57 | false, 58 | SECONDS.toMillis(DEFAULT_DNS_TIMEOUT_SECONDS), 59 | HOURS.toMillis(DEFAULT_RETENTION_DURATION_HOURS), 60 | null, 61 | null); 62 | } 63 | 64 | private DnsSrvResolverBuilder( 65 | DnsReporter reporter, 66 | boolean retainData, 67 | boolean cacheLookups, 68 | long dnsLookupTimeoutMillis, 69 | long retentionDurationMillis, 70 | List servers, 71 | Executor executor) { 72 | this.reporter = reporter; 73 | this.retainData = retainData; 74 | this.cacheLookups = cacheLookups; 75 | this.dnsLookupTimeoutMillis = dnsLookupTimeoutMillis; 76 | this.retentionDurationMillis = retentionDurationMillis; 77 | this.servers = servers; 78 | this.executor = executor; 79 | } 80 | 81 | public DnsSrvResolver build() { 82 | Resolver resolver; 83 | try { 84 | // If the user specified DNS servers, create a new ExtendedResolver which uses them. 85 | // Otherwise, use the default constructor. That will use the servers in ResolverConfig, 86 | // or if that's empty, localhost. 87 | resolver = servers == null ? 88 | new ExtendedResolver() : 89 | new ExtendedResolver(servers.toArray(new String[0])); 90 | } catch (UnknownHostException e) { 91 | throw new RuntimeException(e); 92 | } 93 | 94 | // Configure the Resolver to use our timeouts. 95 | final Duration timeoutDuration = Duration.ofMillis(dnsLookupTimeoutMillis); 96 | resolver.setTimeout(timeoutDuration); 97 | 98 | LookupFactory lookupFactory = executor == null ? new SimpleLookupFactory(resolver, ForkJoinPool.commonPool()) : 99 | new SimpleLookupFactory(resolver, executor); 100 | 101 | if (cacheLookups) { 102 | lookupFactory = new CachingLookupFactory(lookupFactory); 103 | } 104 | 105 | DnsSrvResolver result = new XBillDnsSrvResolver(lookupFactory); 106 | 107 | if (reporter != null) { 108 | result = new MeteredDnsSrvResolver(result, reporter); 109 | } 110 | 111 | if (retainData) { 112 | result = new RetainingDnsSrvResolver(result, retentionDurationMillis); 113 | } 114 | 115 | return result; 116 | } 117 | 118 | public DnsSrvResolverBuilder metered(DnsReporter reporter) { 119 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 120 | retentionDurationMillis, servers, executor); 121 | } 122 | 123 | public DnsSrvResolverBuilder retainingDataOnFailures(boolean retainData) { 124 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 125 | retentionDurationMillis, servers, executor); 126 | } 127 | 128 | /** 129 | * @deprecated 130 | * CachingLookups will be removed in the future as it doesn't work with `resolveAsync` 131 | */ 132 | @Deprecated 133 | public DnsSrvResolverBuilder cachingLookups(boolean cacheLookups) { 134 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 135 | retentionDurationMillis, servers, executor); 136 | } 137 | 138 | public DnsSrvResolverBuilder dnsLookupTimeoutMillis(long dnsLookupTimeoutMillis) { 139 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 140 | retentionDurationMillis, servers, executor); 141 | } 142 | 143 | public DnsSrvResolverBuilder retentionDurationMillis(long retentionDurationMillis) { 144 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 145 | retentionDurationMillis, servers, executor); 146 | } 147 | 148 | public DnsSrvResolverBuilder executor(Executor executor) { 149 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 150 | retentionDurationMillis, servers, executor); 151 | } 152 | 153 | /** 154 | * Allows the user to specify which DNS servers should be used to perform DNS lookups. Servers 155 | * can be specified using either hostname or IP address. If not specified, the underlying DNS 156 | * library will determine which servers to use according to the steps documented in 157 | * 158 | * ResolverConfig.java 159 | * @param servers the DNS servers to use 160 | * @return this builder 161 | */ 162 | public DnsSrvResolverBuilder servers(List servers) { 163 | return new DnsSrvResolverBuilder(reporter, retainData, cacheLookups, dnsLookupTimeoutMillis, 164 | retentionDurationMillis, servers, executor); 165 | } 166 | } 167 | 168 | private DnsSrvResolvers() { 169 | // prevent instantiation 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsSrvWatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import java.io.Closeable; 20 | 21 | /** 22 | * A watcher for DNS SRV records. 23 | * 24 | *

The records can be of any type. Usually something that directly reflects what your 25 | * application will use the records for. 26 | * 27 | * @param The record type 28 | */ 29 | public interface DnsSrvWatcher extends Closeable { 30 | 31 | /** 32 | * Starts watching a FQDN, by creating a {@link ChangeNotifier} for it. 33 | * 34 | * @param fqdn The FQDN to watch 35 | * @return A change notifier that will reflect changes to the watched fqdn 36 | */ 37 | ChangeNotifier watch(String fqdn); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsSrvWatcherFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | /** 20 | * A factory for creating {@link DnsSrvWatcher} implementations. 21 | * 22 | *

A {@link ChangeNotifierFactory} is supplied for creating 23 | * {@link ChangeNotifierFactory.RunnableChangeNotifier}s. It is up to the implementation of the 24 | * {@link DnsSrvWatcher} to decide how to schedule running of the created {@link ChangeNotifier}s. 25 | * 26 | * @param The record type 27 | */ 28 | @FunctionalInterface 29 | public interface DnsSrvWatcherFactory { 30 | 31 | /** 32 | * Creates a {@link DnsSrvWatcher} that should create {@link ChangeNotifier} instances using 33 | * the given factory. 34 | * 35 | * @param changeNotifierFactory The factory to use for creating change notifier instances 36 | * @return A {@link DnsSrvWatcher} 37 | */ 38 | DnsSrvWatcher create(ChangeNotifierFactory changeNotifierFactory); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/DnsSrvWatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import com.google.common.util.concurrent.MoreExecutors; 20 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 21 | 22 | import java.util.concurrent.ScheduledExecutorService; 23 | import java.util.concurrent.ScheduledThreadPoolExecutor; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.function.Function; 26 | 27 | import static com.google.common.base.Preconditions.checkArgument; 28 | import static com.google.common.base.Preconditions.checkState; 29 | import static java.util.Objects.requireNonNull; 30 | import static java.util.concurrent.TimeUnit.SECONDS; 31 | 32 | /** 33 | * Provides builders for configuring and instantiating {@link DnsSrvWatcher}s. 34 | */ 35 | public final class DnsSrvWatchers { 36 | 37 | /** 38 | * Creates a {@link DnsSrvWatcherBuilder} using the given {@link DnsSrvResolver}. The builder 39 | * can be configured to have the desired behavior. 40 | * 41 | *

Exactly one of {@link DnsSrvWatcherBuilder#polling(long, TimeUnit)} or 42 | * {@link DnsSrvWatcherBuilder#customTrigger(DnsSrvWatcherFactory)} must be used. 43 | * 44 | * @param resolver The resolver to use for lookups 45 | * @return a builder for further configuring the watcher 46 | */ 47 | public static DnsSrvWatcherBuilder newBuilder(DnsSrvResolver resolver) { 48 | requireNonNull(resolver, "resolver"); 49 | 50 | return new DnsSrvWatcherBuilder<>(resolver, Function.identity()); 51 | } 52 | 53 | /** 54 | * Creates a {@link DnsSrvWatcherBuilder} using the given {@link DnsSrvResolver}. The builder 55 | * can be configured to have the desired behavior. 56 | * 57 | *

This watcher will use a function that transforms the {@link LookupResult}s into an 58 | * arbitrary type that will be used throughout the {@link DnsSrvWatcher} api. 59 | * 60 | *

Exactly one of {@link DnsSrvWatcherBuilder#polling(long, TimeUnit)} or 61 | * {@link DnsSrvWatcherBuilder#customTrigger(DnsSrvWatcherFactory)} must be used. 62 | * 63 | * @param resolver The resolver to use for lookups 64 | * @param resultTransformer The transformer function 65 | * @return a builder for further configuring the watcher 66 | */ 67 | public static DnsSrvWatcherBuilder newBuilder( 68 | DnsSrvResolver resolver, 69 | Function resultTransformer) { 70 | 71 | requireNonNull(resolver, "resolver"); 72 | requireNonNull(resultTransformer, "resultTransformer"); 73 | 74 | return new DnsSrvWatcherBuilder<>(resolver, resultTransformer); 75 | } 76 | 77 | /** 78 | * @deprecated Use {@link #newBuilder(DnsSrvResolver, java.util.function.Function)} 79 | * deprecated since version 3.2.0 80 | */ 81 | public static DnsSrvWatcherBuilder newBuilder( 82 | DnsSrvResolver resolver, 83 | com.google.common.base.Function resultTransformer) { 84 | 85 | requireNonNull(resolver, "resolver"); 86 | requireNonNull(resultTransformer, "resultTransformer"); 87 | 88 | return new DnsSrvWatcherBuilder<>(resolver, resultTransformer); 89 | } 90 | 91 | public static final class DnsSrvWatcherBuilder { 92 | 93 | private final DnsSrvResolver resolver; 94 | private final Function resultTransformer; 95 | 96 | private final boolean polling; 97 | private final long pollingInterval; 98 | private final TimeUnit pollingIntervalUnit; 99 | 100 | private final ErrorHandler errorHandler; 101 | 102 | private final DnsSrvWatcherFactory dnsSrvWatcherFactory; 103 | 104 | private final ScheduledExecutorService scheduledExecutorService; 105 | 106 | private DnsSrvWatcherBuilder( 107 | DnsSrvResolver resolver, 108 | Function resultTransformer) { 109 | this(resolver, resultTransformer, false, 0, null, null, null, null); 110 | } 111 | 112 | private DnsSrvWatcherBuilder( 113 | DnsSrvResolver resolver, 114 | Function resultTransformer, 115 | boolean polling, 116 | long pollingInterval, 117 | TimeUnit pollingIntervalUnit, 118 | ErrorHandler errorHandler, 119 | DnsSrvWatcherFactory dnsSrvWatcherFactory, 120 | ScheduledExecutorService scheduledExecutorService) { 121 | this.resolver = resolver; 122 | this.resultTransformer = resultTransformer; 123 | this.polling = polling; 124 | this.pollingInterval = pollingInterval; 125 | this.pollingIntervalUnit = pollingIntervalUnit; 126 | this.errorHandler = errorHandler; 127 | this.dnsSrvWatcherFactory = dnsSrvWatcherFactory; 128 | this.scheduledExecutorService = scheduledExecutorService; 129 | } 130 | 131 | public DnsSrvWatcher build() { 132 | checkState(polling ^ dnsSrvWatcherFactory != null, "specify either polling or custom trigger"); 133 | 134 | DnsSrvWatcherFactory watcherFactory; 135 | if (polling) { 136 | final ScheduledExecutorService executor = 137 | scheduledExecutorService != null 138 | ? scheduledExecutorService 139 | : MoreExecutors.getExitingScheduledExecutorService( 140 | new ScheduledThreadPoolExecutor( 141 | 1, new ThreadFactoryBuilder().setNameFormat("dns-lookup-%d").build()), 142 | 0, SECONDS); 143 | 144 | watcherFactory = 145 | cnf -> new PollingDnsSrvWatcher<>(cnf, executor, pollingInterval, pollingIntervalUnit); 146 | } else { 147 | watcherFactory = requireNonNull(dnsSrvWatcherFactory, "dnsSrvWatcherFactory"); 148 | } 149 | 150 | final ChangeNotifierFactory changeNotifierFactory = 151 | fqdn -> new ServiceResolvingChangeNotifier<>( 152 | resolver, fqdn, resultTransformer, errorHandler); 153 | 154 | return watcherFactory.create(changeNotifierFactory); 155 | } 156 | 157 | public DnsSrvWatcherBuilder polling(long pollingInterval, TimeUnit pollingIntervalUnit) { 158 | checkArgument(pollingInterval > 0); 159 | requireNonNull(pollingIntervalUnit, "pollingIntervalUnit"); 160 | 161 | return new DnsSrvWatcherBuilder(resolver, resultTransformer, true, pollingInterval, 162 | pollingIntervalUnit, errorHandler, dnsSrvWatcherFactory, 163 | scheduledExecutorService); 164 | } 165 | 166 | public DnsSrvWatcherBuilder usingExecutor(ScheduledExecutorService scheduledExecutorService) { 167 | return new DnsSrvWatcherBuilder(resolver, resultTransformer, polling, pollingInterval, 168 | pollingIntervalUnit, errorHandler, dnsSrvWatcherFactory, 169 | scheduledExecutorService); 170 | } 171 | 172 | public DnsSrvWatcherBuilder customTrigger(DnsSrvWatcherFactory watcherFactory) { 173 | requireNonNull(watcherFactory, "watcherFactory"); 174 | 175 | return new DnsSrvWatcherBuilder(resolver, resultTransformer, true, pollingInterval, 176 | pollingIntervalUnit, errorHandler, watcherFactory, 177 | scheduledExecutorService); 178 | } 179 | 180 | public DnsSrvWatcherBuilder withErrorHandler(ErrorHandler errorHandler) { 181 | requireNonNull(errorHandler, "errorHandler"); 182 | 183 | return new DnsSrvWatcherBuilder(resolver, resultTransformer, true, pollingInterval, 184 | pollingIntervalUnit, errorHandler, dnsSrvWatcherFactory, 185 | scheduledExecutorService); 186 | } 187 | } 188 | 189 | private DnsSrvWatchers() { 190 | // prevent instantiation 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | /** 20 | * Error handler callback for errors thrown by the {@link DnsSrvResolver} used in a 21 | * {@link DnsSrvWatcher}. 22 | */ 23 | public interface ErrorHandler { 24 | 25 | /** 26 | * Handles a {@link DnsException} for the given FQDN. 27 | * 28 | * @param fqdn The FQDN that was resolved 29 | * @param exception The exception thrown 30 | */ 31 | void handle(String fqdn, DnsException exception); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/LookupFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import org.xbill.DNS.Lookup; 20 | import org.xbill.DNS.lookup.LookupSession; 21 | 22 | /** 23 | * Library-internal interface used for finding or creating {@link LookupSession} or {@link Lookup} instances. 24 | */ 25 | interface LookupFactory { 26 | /** 27 | * Returns a {@link Lookup} instance capable of doing SRV lookups for the supplied FQDN. 28 | * @deprecated 29 | * This synchronous method is being deprecated. 30 | * Use {@link LookupFactory#sessionForName(String)} instead 31 | * 32 | * @param fqdn the name to do lookups for 33 | * @return a Lookup instance 34 | */ 35 | @Deprecated 36 | Lookup forName(String fqdn); 37 | 38 | /** 39 | * Returns a {@link LookupSession} instance capable of doing SRV lookups for the supplied FQDN. 40 | * @param fqdn the name to do lookups for 41 | * @return a Lookup instance 42 | */ 43 | LookupSession sessionForName(String fqdn); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/LookupResult.java: -------------------------------------------------------------------------------- 1 | package com.spotify.dns; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | /** 6 | * Immutable data object with the relevant parts of an SRV record. 7 | */ 8 | public class LookupResult { 9 | 10 | private final String host; 11 | private final int port; 12 | private final int priority; 13 | private final int weight; 14 | private final long ttl; 15 | 16 | private LookupResult(final String host, final int port, final int priority, final int weight, 17 | final long ttl) { 18 | this.host = requireNonNull(host, "host"); 19 | this.port = port; 20 | this.priority = priority; 21 | this.weight = weight; 22 | this.ttl = ttl; 23 | } 24 | 25 | public static LookupResult create(String host, int port, int priority, int weight, long ttl) { 26 | return new LookupResult(host, port, priority, weight, ttl); 27 | } 28 | 29 | public String host() { 30 | return host; 31 | } 32 | 33 | public int port() { 34 | return port; 35 | } 36 | 37 | public int priority() { 38 | return priority; 39 | } 40 | 41 | public int weight() { 42 | return weight; 43 | } 44 | 45 | public long ttl() { 46 | return ttl; 47 | } 48 | 49 | @Override 50 | public boolean equals(final Object o) { 51 | if (this == o) { 52 | return true; 53 | } 54 | if (o == null || getClass() != o.getClass()) { 55 | return false; 56 | } 57 | 58 | final LookupResult that = (LookupResult) o; 59 | 60 | if (port != that.port) { 61 | return false; 62 | } 63 | if (priority != that.priority) { 64 | return false; 65 | } 66 | if (weight != that.weight) { 67 | return false; 68 | } 69 | if (ttl != that.ttl) { 70 | return false; 71 | } 72 | return !(host != null ? !host.equals(that.host) : that.host != null); 73 | 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | int result = host != null ? host.hashCode() : 0; 79 | result = 31 * result + port; 80 | result = 31 * result + priority; 81 | result = 31 * result + weight; 82 | result = 31 * result + (int) (ttl ^ (ttl >>> 32)); 83 | return result; 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return "LookupResult{" + 89 | "host='" + host + '\'' + 90 | ", port=" + port + 91 | ", priority=" + priority + 92 | ", weight=" + weight + 93 | ", ttl=" + ttl + 94 | '}'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/MeteredDnsSrvResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static com.google.common.base.Throwables.throwIfUnchecked; 20 | import static java.util.Objects.requireNonNull; 21 | 22 | import com.spotify.dns.statistics.DnsReporter; 23 | import com.spotify.dns.statistics.DnsTimingContext; 24 | 25 | import java.util.List; 26 | import java.util.concurrent.CompletionStage; 27 | 28 | /** 29 | * Tracks metrics for DnsSrvResolver calls. 30 | */ 31 | class MeteredDnsSrvResolver implements DnsSrvResolver { 32 | private final DnsSrvResolver delegate; 33 | private final DnsReporter reporter; 34 | 35 | MeteredDnsSrvResolver(DnsSrvResolver delegate, DnsReporter reporter) { 36 | this.delegate = requireNonNull(delegate, "delegate"); 37 | this.reporter = requireNonNull(reporter, "reporter"); 38 | } 39 | 40 | @Override 41 | public List resolve(String fqdn) { 42 | // Only catch and report RuntimeException to avoid Error's since that would 43 | // most likely only aggravate any condition that causes them to be thrown. 44 | 45 | final DnsTimingContext resolveTimer = reporter.resolveTimer(); 46 | 47 | final List result; 48 | 49 | try { 50 | result = delegate.resolve(fqdn); 51 | } catch (RuntimeException error) { 52 | reporter.reportFailure(error); 53 | throw error; 54 | } finally { 55 | resolveTimer.stop(); 56 | } 57 | 58 | if (result.isEmpty()) { 59 | reporter.reportEmpty(); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | @Override 66 | public CompletionStage> resolveAsync(String fqdn) { 67 | // Only catch and report RuntimeException to avoid Error's since that would 68 | // most likely only aggravate any condition that causes them to be thrown. 69 | 70 | final DnsTimingContext resolveTimer = reporter.resolveTimer(); 71 | 72 | return delegate 73 | .resolveAsync(fqdn) 74 | .handle( 75 | (result, error) -> { 76 | resolveTimer.stop(); 77 | if (error == null) { 78 | if (result.isEmpty()) { 79 | reporter.reportEmpty(); 80 | } 81 | 82 | return result; 83 | } else { 84 | reporter.reportFailure(error); 85 | throwIfUnchecked(error); 86 | throw new RuntimeException(error); 87 | } 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/PollingDnsSrvWatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import java.io.IOException; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.ScheduledFuture; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static com.spotify.dns.ChangeNotifierFactory.RunnableChangeNotifier; 25 | import static java.util.Objects.requireNonNull; 26 | 27 | class PollingDnsSrvWatcher implements DnsSrvWatcher { 28 | 29 | private final ChangeNotifierFactory changeNotifierFactory; 30 | 31 | private final ScheduledExecutorService executor; 32 | 33 | private final long pollingInterval; 34 | private final TimeUnit pollingIntervalUnit; 35 | 36 | PollingDnsSrvWatcher(ChangeNotifierFactory changeNotifierFactory, 37 | ScheduledExecutorService executor, 38 | long pollingInterval, 39 | TimeUnit pollingIntervalUnit) { 40 | this.changeNotifierFactory = requireNonNull(changeNotifierFactory, "changeNotifierFactory"); 41 | this.executor = requireNonNull(executor, "executor"); 42 | this.pollingInterval = pollingInterval; 43 | this.pollingIntervalUnit = requireNonNull(pollingIntervalUnit, "pollingIntervalUnit"); 44 | } 45 | 46 | @Override 47 | public ChangeNotifier watch(String fqdn) { 48 | final RunnableChangeNotifier changeNotifier = changeNotifierFactory.create(fqdn); 49 | 50 | final ScheduledFuture updaterFuture = 51 | executor.scheduleWithFixedDelay(changeNotifier, 0, pollingInterval, pollingIntervalUnit); 52 | 53 | return changeNotifier; 54 | } 55 | 56 | @Override 57 | public void close() throws IOException { 58 | executor.shutdownNow(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/RetainingDnsSrvResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static com.google.common.base.Throwables.throwIfUnchecked; 20 | import static java.util.Objects.requireNonNull; 21 | 22 | import com.google.common.base.Preconditions; 23 | import com.google.common.cache.Cache; 24 | import com.google.common.cache.CacheBuilder; 25 | import java.util.List; 26 | import java.util.concurrent.CompletionStage; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | /** 30 | * A DnsSrvResolver that keeps track of the previous results of a particular query. If 31 | * available, the previous result is returned in case of a failure, or if a query that used to 32 | * return valid data starts returning empty results. The purpose is to provide protection against 33 | * transient failures in the DNS infrastructure. Data is retained for a configurable period of time. 34 | */ 35 | class RetainingDnsSrvResolver implements DnsSrvResolver { 36 | private final DnsSrvResolver delegate; 37 | private final Cache> cache; 38 | 39 | RetainingDnsSrvResolver(DnsSrvResolver delegate, long retentionTimeMillis) { 40 | Preconditions.checkArgument(retentionTimeMillis > 0L, 41 | "retention time must be positive, was %d", retentionTimeMillis); 42 | 43 | this.delegate = requireNonNull(delegate, "delegate"); 44 | cache = CacheBuilder.newBuilder() 45 | .expireAfterWrite(retentionTimeMillis, TimeUnit.MILLISECONDS) 46 | .build(); 47 | } 48 | 49 | @Override 50 | public List resolve(final String fqdn) { 51 | requireNonNull(fqdn, "fqdn"); 52 | 53 | try { 54 | final List nodes = delegate.resolve(fqdn); 55 | 56 | // No nodes resolved? Return stale data. 57 | if (nodes.isEmpty()) { 58 | List cached = cache.getIfPresent(fqdn); 59 | return (cached != null) ? cached : nodes; 60 | } 61 | 62 | cache.put(fqdn, nodes); 63 | 64 | return nodes; 65 | } catch (Exception e) { 66 | if (cache.getIfPresent(fqdn) != null) { 67 | return cache.getIfPresent(fqdn); 68 | } 69 | 70 | throwIfUnchecked(e); 71 | throw new RuntimeException(e); 72 | } 73 | } 74 | 75 | @Override 76 | public CompletionStage> resolveAsync(final String fqdn) { 77 | requireNonNull(fqdn, "fqdn"); 78 | return delegate.resolveAsync(fqdn).handle((nodes, e) -> { 79 | if (e == null){ 80 | // No nodes resolved? Return stale data. 81 | if (nodes.isEmpty()) { 82 | List cached = cache.getIfPresent(fqdn); 83 | return (cached != null) ? cached : nodes; 84 | } 85 | 86 | cache.put(fqdn, nodes); 87 | 88 | return nodes; 89 | } else{ 90 | if (cache.getIfPresent(fqdn) != null) { 91 | return cache.getIfPresent(fqdn); 92 | } 93 | 94 | throwIfUnchecked(e); 95 | throw new RuntimeException(e); 96 | } 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/ServiceResolvingChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import com.google.common.collect.ImmutableSet; 22 | import java.util.Set; 23 | import java.util.function.Function; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | /** 28 | * A {@link ChangeNotifier} that resolves and provides records using a {@link DnsSrvResolver}. 29 | * 30 | *

The records are refreshable when {@link #run()} is called. 31 | */ 32 | class ServiceResolvingChangeNotifier extends AbstractChangeNotifier 33 | implements ChangeNotifierFactory.RunnableChangeNotifier { 34 | 35 | private static final Logger log = LoggerFactory.getLogger(ServiceResolvingChangeNotifier.class); 36 | 37 | private final DnsSrvResolver resolver; 38 | private final String fqdn; 39 | private final Function resultTransformer; 40 | 41 | private final ErrorHandler errorHandler; 42 | 43 | private volatile Set records = ChangeNotifiers.initialEmptyDataInstance(); 44 | private volatile boolean waitingForFirstEvent = true; 45 | 46 | private volatile boolean run = true; 47 | 48 | /** 49 | * Create a {@link ChangeNotifier} that tracks changes from a {@link DnsSrvResolver}. 50 | * 51 | *

The list of {@link LookupResult}s will be transformed using the provided function 52 | * and put into a set. The set will then be compared to the previous set and if a 53 | * change is detected, the notifier will fire. 54 | * 55 | *

An optional {@link ErrorHandler} can be used to react on {@link DnsException}s thrown 56 | * by the {@link DnsSrvResolver}. 57 | * 58 | * @param resolver The resolver to use. 59 | * @param fqdn The name to lookup SRV records for 60 | * @param resultTransformer The transform function 61 | * @param errorHandler The error handler that will receive exceptions (nullable) 62 | */ 63 | ServiceResolvingChangeNotifier(final DnsSrvResolver resolver, 64 | final String fqdn, 65 | final Function resultTransformer, 66 | final ErrorHandler errorHandler) { 67 | 68 | this.resolver = requireNonNull(resolver, "resolver"); 69 | this.fqdn = requireNonNull(fqdn, "fqdn"); 70 | this.resultTransformer = requireNonNull(resultTransformer, "resultTransformer"); 71 | this.errorHandler = errorHandler; 72 | } 73 | 74 | @Override 75 | protected void closeImplementation() { 76 | run = false; 77 | } 78 | 79 | @Override 80 | public Set current() { 81 | return records; 82 | } 83 | 84 | @Override 85 | public void run() { 86 | if (!run) { 87 | return; 88 | } 89 | 90 | resolver.resolveAsync(fqdn).whenComplete((nodes, e) -> { 91 | if (e instanceof DnsException) { 92 | if (errorHandler != null) { 93 | errorHandler.handle(fqdn, (DnsException) e); 94 | } 95 | log.error(e.getMessage(), e); 96 | fireIfFirstError(); 97 | } else if (e != null) { 98 | log.error(e.getMessage(), e); 99 | fireIfFirstError(); 100 | } else { 101 | final Set current; 102 | try { 103 | ImmutableSet.Builder builder = ImmutableSet.builder(); 104 | for (LookupResult node : nodes) { 105 | T transformed = resultTransformer.apply(node); 106 | builder.add(requireNonNull(transformed, "transformed")); 107 | } 108 | current = builder.build(); 109 | } catch (Exception transformerException) { 110 | log.error(transformerException.getMessage(), transformerException); 111 | fireIfFirstError(); 112 | return; 113 | } 114 | 115 | if (ChangeNotifiers.isNoLongerInitial(current, records) || !current.equals(records)) { 116 | // This means that any subsequent DNS error will be ignored and the existing result will be kept 117 | waitingForFirstEvent = false; 118 | final ChangeNotification changeNotification = 119 | newChangeNotification(current, records); 120 | records = current; 121 | 122 | fireRecordsUpdated(changeNotification); 123 | } 124 | } 125 | }); 126 | } 127 | 128 | private void fireIfFirstError() { 129 | if (waitingForFirstEvent) { 130 | waitingForFirstEvent = false; 131 | Set previous = current(); 132 | records = ImmutableSet.of(); 133 | fireRecordsUpdated(newChangeNotification(records, previous)); 134 | } 135 | } 136 | } 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/SimpleLookupFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import org.xbill.DNS.DClass; 22 | import org.xbill.DNS.Lookup; 23 | import org.xbill.DNS.Resolver; 24 | import org.xbill.DNS.TextParseException; 25 | import org.xbill.DNS.Type; 26 | import org.xbill.DNS.lookup.LookupSession; 27 | 28 | import java.util.concurrent.Executor; 29 | import java.util.concurrent.ForkJoinPool; 30 | 31 | /** A LookupFactory that always returns new instances. */ 32 | public class SimpleLookupFactory implements LookupFactory { 33 | private final LookupSession session; 34 | private final Resolver resolver; 35 | 36 | /** 37 | * @deprecated 38 | * Deprecated to avoid overloading forkjoin common pool. 39 | * Use {@link SimpleLookupFactory#SimpleLookupFactory(Executor)} instead. 40 | */ 41 | @Deprecated 42 | public SimpleLookupFactory() { 43 | this(Lookup.getDefaultResolver()); 44 | } 45 | 46 | /** 47 | * @deprecated 48 | * Deprecated to avoid overloading forkjoin common pool. 49 | * Use {@link SimpleLookupFactory#SimpleLookupFactory(Resolver, Executor)} instead. 50 | */ 51 | public SimpleLookupFactory(Resolver resolver) { 52 | this(resolver, ForkJoinPool.commonPool()); 53 | } 54 | 55 | public SimpleLookupFactory(Executor executor) { 56 | this(Lookup.getDefaultResolver(), executor); 57 | } 58 | 59 | public SimpleLookupFactory(Resolver resolver, Executor executor) { 60 | requireNonNull(executor); 61 | this.resolver = resolver; 62 | this.session = LookupSession.builder().resolver(resolver).executor(executor).build(); 63 | } 64 | 65 | @Override 66 | public Lookup forName(String fqdn) { 67 | try { 68 | final Lookup lookup = new Lookup(fqdn, Type.SRV, DClass.IN); 69 | if (resolver != null) { 70 | lookup.setResolver(resolver); 71 | } 72 | return lookup; 73 | } catch (TextParseException e) { 74 | throw new DnsException("unable to create lookup for name: " + fqdn, e); 75 | } 76 | } 77 | 78 | @Override 79 | public LookupSession sessionForName(String fqdn) { 80 | return session; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/StaticChangeNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import com.google.common.collect.ImmutableSet; 20 | 21 | import java.util.Set; 22 | 23 | /** 24 | * A {@link ChangeNotifier} that provides a static set of records. 25 | */ 26 | class StaticChangeNotifier extends AbstractChangeNotifier { 27 | 28 | private final Set records; 29 | 30 | /** 31 | * Create a static {@link ChangeNotifier}. 32 | * 33 | * @param records The records to provide. 34 | */ 35 | StaticChangeNotifier(final Set records) { 36 | this.records = ImmutableSet.copyOf(records); 37 | } 38 | 39 | @Override 40 | public Set current() { 41 | return records; 42 | } 43 | 44 | @Override 45 | protected void closeImplementation() { 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/XBillDnsSrvResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static java.util.Objects.requireNonNull; 20 | 21 | import com.google.common.collect.ImmutableList; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.xbill.DNS.DClass; 26 | import org.xbill.DNS.Lookup; 27 | import org.xbill.DNS.Name; 28 | import org.xbill.DNS.Record; 29 | import org.xbill.DNS.SRVRecord; 30 | import org.xbill.DNS.TextParseException; 31 | import org.xbill.DNS.Type; 32 | import org.xbill.DNS.lookup.LookupSession; 33 | import org.xbill.DNS.lookup.NoSuchDomainException; 34 | import org.xbill.DNS.lookup.NoSuchRRSetException; 35 | 36 | import java.util.List; 37 | import java.util.concurrent.CompletionException; 38 | import java.util.concurrent.CompletionStage; 39 | 40 | /** 41 | * A DnsSrvResolver implementation that uses the dnsjava implementation: 42 | * https://github.com/dnsjava/dnsjava 43 | */ 44 | class XBillDnsSrvResolver implements DnsSrvResolver { 45 | private static final Logger LOG = LoggerFactory.getLogger(XBillDnsSrvResolver.class); 46 | 47 | private final LookupFactory lookupFactory; 48 | 49 | XBillDnsSrvResolver(LookupFactory lookupFactory) { 50 | this.lookupFactory = requireNonNull(lookupFactory, "lookupFactory"); 51 | } 52 | 53 | @Override 54 | public List resolve(final String fqdn) { 55 | Lookup lookup = lookupFactory.forName(fqdn); 56 | Record[] queryResult = lookup.run(); 57 | 58 | switch (lookup.getResult()) { 59 | case Lookup.SUCCESSFUL: 60 | return toLookupResults(queryResult); 61 | case Lookup.HOST_NOT_FOUND: 62 | // fallthrough 63 | case Lookup.TYPE_NOT_FOUND: 64 | LOG.warn("No results returned for query '{}'; result from XBill: {} - {}", 65 | fqdn, lookup.getResult(), lookup.getErrorString()); 66 | return ImmutableList.of(); 67 | default: 68 | throw new DnsException( 69 | String.format("Lookup of '%s' failed with code: %d - %s ", 70 | fqdn, lookup.getResult(), lookup.getErrorString())); 71 | } 72 | } 73 | 74 | @Override 75 | public CompletionStage> resolveAsync(final String fqdn) { 76 | LookupSession lookup = lookupFactory.sessionForName(fqdn); 77 | Name name; 78 | try { 79 | name = Name.fromString(fqdn); 80 | } catch (TextParseException e) { 81 | throw new DnsException("unable to create lookup for name: " + fqdn, e); 82 | } 83 | 84 | return lookup.lookupAsync(name, Type.SRV, DClass.IN).handle((result, ex) ->{ 85 | if (ex == null){ 86 | return toLookupResults(result); 87 | } else{ 88 | Throwable cause = ex; 89 | if (ex instanceof CompletionException && ex.getCause() != null) { 90 | cause = ex.getCause(); 91 | } 92 | if (cause instanceof NoSuchRRSetException || cause instanceof NoSuchDomainException) { 93 | LOG.warn("No results returned for query '{}'; result from dnsjava: {}", 94 | fqdn, ex.getMessage()); 95 | return ImmutableList.of(); 96 | } 97 | throw new DnsException( 98 | String.format("Lookup of '%s' failed: %s ", fqdn, ex.getMessage()), ex); 99 | } 100 | }); 101 | } 102 | 103 | private static List toLookupResults(org.xbill.DNS.lookup.LookupResult queryResult) { 104 | ImmutableList.Builder builder = ImmutableList.builder(); 105 | 106 | for (Record record: queryResult.getRecords()) { 107 | if (record instanceof SRVRecord) { 108 | SRVRecord srvRecord = (SRVRecord) record; 109 | builder.add(LookupResult.create(srvRecord.getTarget().toString(), 110 | srvRecord.getPort(), 111 | srvRecord.getPriority(), 112 | srvRecord.getWeight(), 113 | srvRecord.getTTL())); 114 | } 115 | } 116 | 117 | return builder.build(); 118 | } 119 | 120 | private static List toLookupResults(Record[] queryResult) { 121 | ImmutableList.Builder builder = ImmutableList.builder(); 122 | 123 | if (queryResult != null) { 124 | for (Record record : queryResult) { 125 | if (record instanceof SRVRecord) { 126 | SRVRecord srvRecord = (SRVRecord) record; 127 | builder.add(LookupResult.create(srvRecord.getTarget().toString(), 128 | srvRecord.getPort(), 129 | srvRecord.getPriority(), 130 | srvRecord.getWeight(), 131 | srvRecord.getTTL())); 132 | } 133 | } 134 | } 135 | 136 | return builder.build(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/statistics/DnsReporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns.statistics; 18 | 19 | /** 20 | * Implement to report statistics for DNS request. 21 | * 22 | * This interface exists to allow implementers to bridge the statistics 23 | * collected through the use of this library with their own statistics solution. 24 | */ 25 | public interface DnsReporter { 26 | /** 27 | * Report resolve timing. 28 | * @return A new timing context. 29 | */ 30 | DnsTimingContext resolveTimer(); 31 | 32 | /** 33 | * Report that an empty response has been received from a resolve. 34 | */ 35 | void reportEmpty(); 36 | 37 | /** 38 | * Report that a resolve resulting in a failure. 39 | * @param error The exception causing the failure. 40 | */ 41 | void reportFailure(Throwable error); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/dns/statistics/DnsTimingContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns.statistics; 18 | 19 | /** 20 | * Implement to handle timings when performing dns requests. 21 | */ 22 | @FunctionalInterface 23 | public interface DnsTimingContext { 24 | void stop(); 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/AbstractChangeNotifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.hamcrest.Matchers.containsInAnyOrder; 21 | import static org.junit.Assert.assertThat; 22 | import static org.mockito.Matchers.any; 23 | import static org.mockito.Mockito.doThrow; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.never; 26 | import static org.mockito.Mockito.verify; 27 | 28 | import java.util.Set; 29 | 30 | import com.google.common.collect.Sets; 31 | import org.junit.Before; 32 | import org.junit.Rule; 33 | import org.junit.Test; 34 | import org.junit.rules.ExpectedException; 35 | import org.mockito.ArgumentCaptor; 36 | import org.mockito.Mock; 37 | import org.mockito.MockitoAnnotations; 38 | 39 | public class AbstractChangeNotifierTest { 40 | 41 | @Mock 42 | public ChangeNotifier.Listener listener; 43 | 44 | @Mock 45 | public ChangeNotifier.ChangeNotification changeNotification; 46 | 47 | @Rule 48 | public ExpectedException thrown = ExpectedException.none(); 49 | 50 | AbstractChangeNotifier sut; 51 | 52 | @Before 53 | public void setUp() { 54 | MockitoAnnotations.initMocks(this); 55 | 56 | sut = new AbstractChangeNotifier() { 57 | @Override 58 | public Set current() { 59 | return Sets.newHashSet("foo", "bar"); 60 | } 61 | 62 | @Override 63 | public void closeImplementation() { 64 | } 65 | }; 66 | } 67 | 68 | @Test 69 | public void shouldRegisterListener() { 70 | sut.setListener(listener, false); 71 | sut.fireRecordsUpdated(changeNotification); 72 | 73 | verify(listener).onChange(changeNotification); 74 | } 75 | 76 | @Test 77 | @SuppressWarnings("unchecked") 78 | public void shouldNotFireImmediatelyIfFalse() { 79 | sut.setListener(listener, false); 80 | 81 | verify(listener, never()).onChange(any(ChangeNotifier.ChangeNotification.class)); 82 | } 83 | 84 | @Test 85 | @SuppressWarnings("unchecked") 86 | public void shouldFireImmediatelyIfTrue() { 87 | sut.setListener(listener, true); 88 | 89 | final ArgumentCaptor captor = 90 | ArgumentCaptor.forClass(ChangeNotifier.ChangeNotification.class); 91 | verify(listener).onChange(captor.capture()); 92 | 93 | final ChangeNotifier.ChangeNotification notification = captor.getValue(); 94 | assertThat(notification.previous().size(), is(0)); 95 | assertThat(notification.current().size(), is(2)); 96 | assertThat(notification.current(), containsInAnyOrder("foo", "bar")); 97 | } 98 | 99 | @Test 100 | @SuppressWarnings("unchecked") 101 | public void shouldNotFireAfterClose() { 102 | sut.setListener(listener, false); 103 | sut.close(); 104 | sut.fireRecordsUpdated(changeNotification); 105 | 106 | verify(listener, never()).onChange(any(ChangeNotifier.ChangeNotification.class)); 107 | } 108 | 109 | @Test 110 | @SuppressWarnings("unchecked") 111 | public void shouldNotAllowMultipleListeners() { 112 | sut.setListener(listener, false); 113 | 114 | thrown.expect(IllegalStateException.class); 115 | sut.setListener(mock(ChangeNotifier.Listener.class), false); 116 | } 117 | 118 | @Test 119 | @SuppressWarnings("unchecked") 120 | public void shouldIgnoreListenerExceptions() { 121 | doThrow(new RuntimeException("stupid listener")) 122 | .when(listener) 123 | .onChange(any(ChangeNotifier.ChangeNotification.class)); 124 | 125 | sut.setListener(listener, false); 126 | sut.fireRecordsUpdated(changeNotification); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/AggregatingChangeNotifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.spotify.dns; 17 | 18 | import static org.mockito.Mockito.any; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.never; 21 | import static org.mockito.Mockito.verify; 22 | import static org.mockito.Mockito.verifyNoMoreInteractions; 23 | 24 | import java.util.Arrays; 25 | import java.util.List; 26 | import java.util.Set; 27 | 28 | import com.google.common.collect.Sets; 29 | import org.junit.Test; 30 | 31 | public class AggregatingChangeNotifierTest { 32 | @Test 33 | public void testEmptySet() { 34 | MyNotifier childNotifier = new MyNotifier(); 35 | AggregatingChangeNotifier notifier = new AggregatingChangeNotifier<>(Arrays.asList(childNotifier)); 36 | 37 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 38 | notifier.setListener(listener, false); 39 | 40 | verify(listener, never()).onChange(any(ChangeNotifier.ChangeNotification.class)); 41 | 42 | childNotifier.set(Sets.newHashSet()); 43 | verifyNoMoreInteractions(listener); 44 | 45 | } 46 | 47 | private static class MyNotifier extends AbstractChangeNotifier { 48 | private volatile Set records = ChangeNotifiers.initialEmptyDataInstance(); 49 | 50 | @Override 51 | protected void closeImplementation() { 52 | } 53 | 54 | @Override 55 | public Set current() { 56 | return records; 57 | } 58 | 59 | public void set(Set records) { 60 | fireRecordsUpdated(newChangeNotification(records, current())); 61 | this.records = records; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/CachingLookupFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.equalTo; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.CoreMatchers.not; 22 | import static org.junit.Assert.assertThat; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | import java.util.concurrent.ExecutorService; 27 | import java.util.concurrent.Executors; 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | import org.xbill.DNS.Lookup; 31 | 32 | public class CachingLookupFactoryTest { 33 | CachingLookupFactory factory; 34 | 35 | LookupFactory delegate; 36 | 37 | Lookup lookup; 38 | Lookup lookup2; 39 | 40 | @Before 41 | public void setUp() throws Exception { 42 | delegate = mock(LookupFactory.class); 43 | 44 | factory = new CachingLookupFactory(delegate); 45 | 46 | lookup = new Lookup("hi"); 47 | lookup2 = new Lookup("hey"); 48 | } 49 | 50 | @Test 51 | public void shouldReturnResultsFromDelegate() { 52 | when(delegate.forName("a name")).thenReturn(lookup); 53 | 54 | assertThat(factory.forName("a name"), equalTo(lookup)); 55 | } 56 | 57 | @Test 58 | public void shouldCacheResultsForSubsequentQueries() { 59 | when(delegate.forName("hej")).thenReturn(lookup, lookup2); 60 | 61 | Lookup first = factory.forName("hej"); 62 | Lookup second = factory.forName("hej"); 63 | 64 | assertThat(first == second, is(true)); 65 | } 66 | 67 | @Test 68 | public void shouldReturnDifferentForDifferentQueries() { 69 | when(delegate.forName("hej")).thenReturn(lookup); 70 | when(delegate.forName("hopp")).thenReturn(lookup2); 71 | 72 | Lookup first = factory.forName("hej"); 73 | Lookup second = factory.forName("hopp"); 74 | 75 | assertThat(first == second, is(false)); 76 | } 77 | 78 | @Test 79 | public void shouldReturnDifferentForDifferentThreads() throws Exception { 80 | ExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); 81 | factory = new CachingLookupFactory(new SimpleLookupFactory()); 82 | 83 | Lookup first = factory.forName("hej"); 84 | Lookup second = executorService.submit(() -> factory.forName("hej")).get(); 85 | 86 | assertThat(second, not(equalTo(first))); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/DnsLookupPerformanceTest.java: -------------------------------------------------------------------------------- 1 | package com.spotify.dns; 2 | 3 | import org.junit.Ignore; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.concurrent.CountDownLatch; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | import static org.hamcrest.Matchers.equalTo; 16 | import static org.junit.Assert.assertThat; 17 | 18 | public class DnsLookupPerformanceTest { 19 | private static AtomicInteger successCount = new AtomicInteger(0); 20 | 21 | private static DnsSrvResolver resolver = DnsSrvResolvers.newBuilder() 22 | .cachingLookups(false) 23 | .retainingDataOnFailures(false) 24 | .dnsLookupTimeoutMillis(5000) 25 | .executor(Executors.newFixedThreadPool(10)) 26 | .build(); 27 | 28 | @Test 29 | @Ignore("Needs network access and is timing dependent") 30 | public void runTest() throws InterruptedException { 31 | int numThreads = 3; 32 | final ExecutorService executorService = Executors.newFixedThreadPool(numThreads); 33 | List records = Arrays.asList( 34 | "_spotify-noop._http.services.gew1.spotify.net.", 35 | "_spotify-noop._http.services.guc3.spotify.net.", 36 | "_spotify-noop._http.services.gae2.spotify.net.", 37 | "_spotify-palindrome._grpc.services.gae2.spotify.net.", 38 | "_spotify-palindrome._grpc.services.gew1.spotify.net.", 39 | "_spotify-concat._grpc.services.gew1.spotify.net.", 40 | "_spotify-concat._grpc.services.guc3.spotify.net.", 41 | "_spotify-concat._hm.services.gae2.spotify.net.", 42 | "_spotify-concat._hm.services.gew1.spotify.net.", 43 | "_spotify-concat._hm.services.guc3.spotify.net.", 44 | "_spotify-fabric-test._grpc.services.gae2.spotify.net.", 45 | "_spotify-fabric-test._grpc.services.gew1.spotify.net.", 46 | "_spotify-fabric-test._grpc.services.guc3.spotify.net.", 47 | "_spotify-fabric-test._hm.services.gae2.spotify.net.", 48 | "_spotify-fabric-test._hm.services.gew1.spotify.net.", 49 | "_spotify-fabric-test._hm.services.guc3.spotify.net.", 50 | "_spotify-fabric-load-generator._grpc.services.gae2.spotify.net.", 51 | "_spotify-fabric-load-generator._grpc.services.gew1.spotify.net.", 52 | "_spotify-fabric-load-generator._grpc.services.guc3.spotify.net.", 53 | "_spotify-client._tcp.spotify.com"); 54 | 55 | CountDownLatch done = new CountDownLatch(records.size() * 2); 56 | records.stream() 57 | .forEach( 58 | fqdn -> { 59 | executorService.submit(() -> resolve(fqdn, done)); 60 | CompletableFuture.runAsync(DnsLookupPerformanceTest::blockCommonPool) 61 | .whenComplete((v, ex) -> done.countDown()); 62 | }); 63 | done.await(1, TimeUnit.MINUTES); 64 | executorService.shutdown(); 65 | 66 | int failureCount = records.size() - successCount.get(); 67 | 68 | System.out.println("Number of threads: " + numThreads); 69 | System.out.println("Number of records: " + records.size()); 70 | System.out.println("Failed lookups: " + failureCount); 71 | 72 | assertThat(failureCount, equalTo(0)); 73 | } 74 | 75 | private static void blockCommonPool() { 76 | try { 77 | Thread.sleep(10_000); 78 | } catch (InterruptedException e) { 79 | e.printStackTrace(); 80 | } 81 | } 82 | 83 | private static void resolve(String fqdn, CountDownLatch done) { 84 | try { 85 | System.out.println("Resolving: " + fqdn); 86 | List results = resolver.resolveAsync(fqdn).toCompletableFuture().get(); 87 | 88 | if(!results.isEmpty()) { 89 | successCount.incrementAndGet(); 90 | System.out.println(fqdn + "...ok!"); 91 | } else { 92 | System.err.format("%s ... failed!\n", fqdn); 93 | } 94 | } catch (Exception e) { 95 | System.err.format("%s ... failed!\n", fqdn); 96 | e.printStackTrace(System.err); 97 | } finally { 98 | done.countDown(); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/DnsSrvResolversIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.containsString; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.Matchers.containsInAnyOrder; 22 | import static org.junit.Assert.assertThat; 23 | import static org.junit.Assert.assertTrue; 24 | import static org.mockito.Matchers.isA; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.never; 27 | import static org.mockito.Mockito.times; 28 | import static org.mockito.Mockito.verify; 29 | import static org.mockito.Mockito.when; 30 | 31 | import com.jayway.awaitility.Awaitility; 32 | import com.spotify.dns.statistics.DnsReporter; 33 | import com.spotify.dns.statistics.DnsTimingContext; 34 | import java.util.ArrayList; 35 | import java.util.Arrays; 36 | import java.util.Collections; 37 | import java.util.List; 38 | import java.util.Set; 39 | import java.util.concurrent.ExecutionException; 40 | import java.util.concurrent.TimeUnit; 41 | import org.hamcrest.Matchers; 42 | import org.junit.Before; 43 | import org.junit.Test; 44 | import org.xbill.DNS.SimpleResolver; 45 | 46 | /** 47 | * Integration tests for the DnsSrvResolversIT class. 48 | */ 49 | public class DnsSrvResolversIT { 50 | 51 | private DnsSrvResolver resolver; 52 | 53 | @Before 54 | public void setUp() { 55 | resolver = DnsSrvResolvers.newBuilder().build(); 56 | } 57 | 58 | @Test 59 | public void shouldReturnResultsForValidQuery() throws ExecutionException, InterruptedException { 60 | assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").isEmpty(), is(false)); 61 | assertThat(resolver.resolveAsync("_spotify-client._tcp.spotify.com").toCompletableFuture().get().isEmpty(), is(false)); 62 | } 63 | 64 | @Test 65 | public void testCorrectSequenceOfNotifications() { 66 | ChangeNotifier notifier = ChangeNotifiers.aggregate( 67 | DnsSrvWatchers.newBuilder(resolver) 68 | .polling(100, TimeUnit.MILLISECONDS) 69 | .build().watch("_spotify-client._tcp.spotify.com")); 70 | 71 | final List changes = Collections.synchronizedList(new ArrayList<>()); 72 | 73 | notifier.setListener(changeNotification -> { 74 | Set current = changeNotification.current(); 75 | if (!ChangeNotifiers.isInitialEmptyData(current)) { 76 | changes.add(current.isEmpty() ? "empty" : "data"); 77 | } 78 | }, true); 79 | assertThat(changes, Matchers.empty()); 80 | Awaitility.await().atMost(2, TimeUnit.SECONDS).until(() -> changes.size() >= 1); 81 | assertThat(changes, containsInAnyOrder("data")); 82 | } 83 | 84 | @Test 85 | public void shouldTrackMetricsWhenToldTo() throws ExecutionException, InterruptedException { 86 | final DnsReporter reporter = mock(DnsReporter.class); 87 | final DnsTimingContext timingReporter = mock(DnsTimingContext.class); 88 | 89 | resolver = DnsSrvResolvers.newBuilder() 90 | .metered(reporter) 91 | .build(); 92 | 93 | when(reporter.resolveTimer()).thenReturn(timingReporter); 94 | resolver.resolveAsync("_spotify-client._tcp.sto.spotify.net").toCompletableFuture().get(); 95 | verify(timingReporter).stop(); 96 | verify(reporter, never()).reportFailure(isA(RuntimeException.class)); 97 | verify(reporter, times(1)).reportEmpty(); 98 | } 99 | 100 | @Test 101 | public void shouldFailForBadHostNamesAsync() throws Exception { 102 | try { 103 | resolver.resolveAsync("nonexistenthost").toCompletableFuture().get(); 104 | } 105 | catch (DnsException e) { 106 | assertThat(e.getMessage(), containsString("host not found")); 107 | } 108 | } 109 | 110 | @Test 111 | public void shouldFailForBadHostNames() { 112 | try { 113 | resolver.resolve("nonexistenthost"); 114 | } 115 | catch (DnsException e) { 116 | assertThat(e.getMessage(), containsString("host not found")); 117 | } 118 | } 119 | 120 | @Test 121 | public void shouldReturnResultsUsingSpecifiedServers() throws Exception { 122 | final String server = new SimpleResolver().getAddress().getHostName(); 123 | final DnsSrvResolver resolver = DnsSrvResolvers 124 | .newBuilder() 125 | .servers(Arrays.asList(server)) 126 | .build(); 127 | assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").isEmpty(), is(false)); 128 | assertThat(resolver.resolveAsync("_spotify-client._tcp.spotify.com").toCompletableFuture().get().isEmpty(), is(false)); 129 | } 130 | 131 | @Test 132 | public void shouldSucceedCreatingRetainingDnsResolver() { 133 | try { 134 | resolver = DnsSrvResolvers.newBuilder().retainingDataOnFailures(true).build(); 135 | } 136 | catch (DnsException e) { 137 | assertTrue("DNS exception should not be thrown", false); 138 | } 139 | catch (IllegalArgumentException e) { 140 | assertTrue("Illegal argument exception should not be thrown", false); 141 | } 142 | } 143 | // TODO: it would be nice to be able to also test things like intermittent DNS failures, etc., 144 | // but that takes a lot of work setting up a DNS infrastructure that can be made to fail in a 145 | // controlled way, so I'm skipping that. 146 | } 147 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/DnsSrvWatchersTest.java: -------------------------------------------------------------------------------- 1 | package com.spotify.dns; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.contains; 5 | import static org.hamcrest.Matchers.is; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.concurrent.CompletionStage; 12 | import java.util.concurrent.CountDownLatch; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | import org.junit.Test; 16 | import org.xbill.DNS.lookup.NoSuchDomainException; 17 | 18 | public class DnsSrvWatchersTest { 19 | 20 | @Test 21 | public void noRaceBetweenSetListenerAndPollingForUpdates() throws Exception { 22 | int limit = 20000; 23 | while (limit-- > 0) { 24 | resolve(); 25 | } 26 | } 27 | 28 | private void resolve() throws Exception { 29 | final DnsSrvResolver srvResolver = new FakeResolver( 30 | "horse.sto3.spotify.net", LookupResult.create("localhost", 1, 0, 0, 0)); 31 | 32 | final AtomicReference> hosts = new AtomicReference<>(); 33 | final CountDownLatch latch = new CountDownLatch(1); 34 | 35 | final ChangeNotifier.Listener listener = new FakeListener(hosts, latch); 36 | 37 | DnsSrvWatcher watcher = DnsSrvWatchers.newBuilder(srvResolver) 38 | .polling(1, TimeUnit.MILLISECONDS) 39 | .build(); 40 | final ChangeNotifier notifier = watcher.watch("horse.sto3.spotify.net"); 41 | 42 | // since I set fire to true I'd expect being notified either right away or after a millisecond 43 | notifier.setListener(listener, true); 44 | latch.await(); 45 | notifier.close(); 46 | watcher.close(); 47 | assertThat(hosts.get(), contains(is(LookupResult.create("localhost", 1, 0, 0, 0)))); 48 | } 49 | 50 | static class FakeListener implements ChangeNotifier.Listener { 51 | 52 | private final AtomicReference> hosts; 53 | private final CountDownLatch latch; 54 | 55 | FakeListener(AtomicReference> hosts, CountDownLatch latch) { 56 | this.hosts = hosts; 57 | this.latch = latch; 58 | } 59 | 60 | @Override 61 | public void onChange(ChangeNotifier.ChangeNotification changeNotification) { 62 | 63 | hosts.set(changeNotification.current()); 64 | if (!changeNotification.current().isEmpty()) { 65 | latch.countDown(); 66 | } 67 | 68 | } 69 | } 70 | 71 | static class FakeResolver implements DnsSrvResolver { 72 | 73 | private final String fqdn; 74 | private final LookupResult result; 75 | 76 | public FakeResolver(String fqdn, LookupResult result) { 77 | this.fqdn = fqdn; 78 | this.result = result; 79 | } 80 | 81 | @Override 82 | public List resolve(String fqdn) { 83 | if (this.fqdn.equals(fqdn)) { 84 | return Arrays.asList(result); 85 | } else { 86 | return null; 87 | } 88 | } 89 | 90 | @Override 91 | public CompletionStage> resolveAsync(String fqdn) { 92 | if (this.fqdn.equals(fqdn)) { 93 | return CompletableFuture.completedFuture(Arrays.asList(result)); 94 | } else { 95 | return DnsTestUtil.failedFuture(new DnsException(this.fqdn + " != " + fqdn)); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/DnsTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.Stream; 23 | 24 | /** 25 | * Utility functions that are shared between tests. 26 | */ 27 | public class DnsTestUtil { 28 | static List nodes(String... nodeNames) { 29 | return Stream.of(nodeNames) 30 | .map(input -> LookupResult.create(input, 8080, 1, 2, 999)) 31 | .collect(Collectors.toList()); 32 | } 33 | 34 | /** 35 | * method to replace CompletableFuture.failedFuture() from Java 9 in Java 8 36 | */ 37 | static CompletableFuture> failedFuture(Exception ex) { 38 | CompletableFuture> future = new CompletableFuture<>(); 39 | future.completeExceptionally(ex); 40 | return future; 41 | } 42 | 43 | /** 44 | * method to replace CompletableFuture.failedFuture() from Java 9 in Java 8 45 | */ 46 | static CompletableFuture> failedFuture(Error error) { 47 | CompletableFuture> future = new CompletableFuture<>(); 48 | future.completeExceptionally(error); 49 | return future; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/MeteredDnsSrvResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.fail; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.never; 23 | import static org.mockito.Mockito.verify; 24 | import static org.mockito.Mockito.when; 25 | 26 | import com.spotify.dns.statistics.DnsReporter; 27 | import com.spotify.dns.statistics.DnsTimingContext; 28 | import java.util.List; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.ExecutionException; 31 | 32 | import org.junit.After; 33 | import org.junit.Before; 34 | import org.junit.Test; 35 | 36 | public class MeteredDnsSrvResolverTest { 37 | private static final String FQDN = "nånting"; 38 | private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException(); 39 | private static final Error ERROR = new Error(); 40 | 41 | @SuppressWarnings("unchecked") 42 | private static final List EMPTY = mock(List.class); 43 | @SuppressWarnings("unchecked") 44 | private static final List NOT_EMPTY = mock(List.class); 45 | 46 | static { 47 | when(EMPTY.isEmpty()).thenReturn(true); 48 | when(NOT_EMPTY.isEmpty()).thenReturn(false); 49 | } 50 | 51 | private DnsSrvResolver delegate; 52 | private DnsReporter reporter; 53 | private DnsTimingContext timingReporter; 54 | 55 | private DnsSrvResolver resolver; 56 | 57 | @Before 58 | public void before() { 59 | delegate = mock(DnsSrvResolver.class); 60 | reporter = mock(DnsReporter.class); 61 | timingReporter = mock(DnsTimingContext.class); 62 | 63 | resolver = new MeteredDnsSrvResolver(delegate, reporter); 64 | 65 | when(reporter.resolveTimer()).thenReturn(timingReporter); 66 | } 67 | 68 | @After 69 | public void after() { 70 | // XXX: would be really strange if this was not called under the current circumstances. 71 | verify(reporter).resolveTimer(); 72 | verify(timingReporter).stop(); 73 | } 74 | 75 | @Test 76 | public void shouldCountSuccessful() throws Exception { 77 | when(delegate.resolve(FQDN)).thenReturn(NOT_EMPTY); 78 | 79 | resolver.resolve(FQDN); 80 | 81 | verify(reporter, never()).reportEmpty(); 82 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 83 | } 84 | 85 | @Test 86 | public void shouldCountSuccessfulAsync() throws Exception { 87 | CompletableFuture> completedNotEmpty = CompletableFuture.completedFuture(NOT_EMPTY); 88 | when(delegate.resolveAsync(FQDN)).thenReturn(completedNotEmpty); 89 | 90 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 91 | 92 | verify(reporter, never()).reportEmpty(); 93 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 94 | } 95 | 96 | @Test 97 | public void shouldReportEmpty() throws Exception { 98 | when(delegate.resolve(FQDN)).thenReturn(EMPTY); 99 | 100 | resolver.resolve(FQDN); 101 | 102 | verify(reporter).reportEmpty(); 103 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 104 | } 105 | 106 | @Test 107 | public void shouldReportEmptyAsync() throws Exception { 108 | CompletableFuture> completedEmpty = CompletableFuture.completedFuture(EMPTY); 109 | when(delegate.resolveAsync(FQDN)).thenReturn(completedEmpty); 110 | 111 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 112 | 113 | verify(reporter).reportEmpty(); 114 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 115 | } 116 | 117 | @Test 118 | public void shouldReportRuntimeException() throws Exception { 119 | when(delegate.resolve(FQDN)).thenThrow(RUNTIME_EXCEPTION); 120 | 121 | try { 122 | resolver.resolve(FQDN); 123 | fail("resolve should have thrown exception"); 124 | } catch(RuntimeException e) { 125 | assertEquals(RUNTIME_EXCEPTION, e); 126 | } 127 | 128 | verify(reporter, never()).reportEmpty(); 129 | verify(reporter).reportFailure(RUNTIME_EXCEPTION); 130 | } 131 | 132 | @Test 133 | public void shouldReportRuntimeExceptionAsync() throws Exception { 134 | CompletableFuture> future = new CompletableFuture<>(); 135 | future.completeExceptionally(RUNTIME_EXCEPTION); 136 | when(delegate.resolveAsync(FQDN)).thenReturn(future); 137 | 138 | try { 139 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 140 | fail("resolve should have thrown exception"); 141 | } catch(ExecutionException e) { 142 | assertEquals(RUNTIME_EXCEPTION, e.getCause()); 143 | } 144 | 145 | verify(reporter, never()).reportEmpty(); 146 | verify(reporter).reportFailure(RUNTIME_EXCEPTION); 147 | } 148 | 149 | @Test 150 | public void shouldNotReportError() throws Exception { 151 | when(delegate.resolve(FQDN)).thenThrow(ERROR); 152 | 153 | try { 154 | resolver.resolve(FQDN); 155 | fail("resolve should have thrown exception"); 156 | } catch(Error e) { 157 | assertEquals(ERROR, e); 158 | } 159 | 160 | verify(reporter, never()).reportEmpty(); 161 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 162 | } 163 | 164 | @Test 165 | public void shouldNotReportErrorAsync() throws Exception { 166 | when(delegate.resolveAsync(FQDN)).thenReturn(DnsTestUtil.failedFuture(ERROR)); 167 | 168 | try { 169 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 170 | fail("resolve should have thrown exception"); 171 | } catch(ExecutionException e) { 172 | assertEquals(ERROR, e.getCause()); 173 | } 174 | 175 | verify(reporter, never()).reportEmpty(); 176 | verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/RetainingDnsSrvResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static com.spotify.dns.DnsTestUtil.nodes; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.Matchers.equalTo; 22 | import static org.junit.Assert.assertThat; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | import java.util.List; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.ExecutionException; 29 | 30 | import org.junit.Before; 31 | import org.junit.Rule; 32 | import org.junit.Test; 33 | import org.junit.rules.ExpectedException; 34 | 35 | public class RetainingDnsSrvResolverTest { 36 | private static final String FQDN = "heythere"; 37 | private static final long RETENTION_TIME_MILLIS = 50L; 38 | 39 | RetainingDnsSrvResolver resolver; 40 | 41 | DnsSrvResolver delegate; 42 | 43 | List nodes1; 44 | List nodes2; 45 | 46 | @Rule 47 | public ExpectedException thrown = ExpectedException.none(); 48 | 49 | @Before 50 | public void setUp() { 51 | delegate = mock(DnsSrvResolver.class); 52 | 53 | resolver = new RetainingDnsSrvResolver(delegate, RETENTION_TIME_MILLIS); 54 | 55 | nodes1 = nodes("noden1", "noden2"); 56 | nodes2 = nodes("noden3", "noden5", "somethingelse"); 57 | } 58 | 59 | @Test 60 | public void shouldReturnResultsFromDelegate() { 61 | when(delegate.resolve(FQDN)).thenReturn(nodes1); 62 | 63 | assertThat(resolver.resolve(FQDN), equalTo(nodes1)); 64 | } 65 | 66 | @Test 67 | public void shouldReturnResultsFromDelegateAsync() throws ExecutionException, InterruptedException { 68 | when(delegate.resolveAsync(FQDN)).thenReturn(CompletableFuture.completedFuture(nodes1)); 69 | 70 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get(), equalTo(nodes1)); 71 | } 72 | 73 | @Test 74 | public void shouldReturnResultsFromDelegateEachTime() { 75 | when(delegate.resolve(FQDN)).thenReturn(nodes1).thenReturn(nodes2); 76 | 77 | resolver.resolve(FQDN); 78 | 79 | assertThat(resolver.resolve(FQDN), equalTo(nodes2)); 80 | } 81 | 82 | @Test 83 | public void shouldReturnResultsFromDelegateEachTimeAsync() throws ExecutionException, InterruptedException { 84 | when(delegate.resolveAsync(FQDN)) 85 | .thenReturn(CompletableFuture.completedFuture(nodes1)) 86 | .thenReturn(CompletableFuture.completedFuture(nodes2)); 87 | 88 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 89 | 90 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get(), equalTo(nodes2)); 91 | } 92 | 93 | @Test 94 | public void shouldRetainDataIfNewResultEmpty() { 95 | when(delegate.resolve(FQDN)).thenReturn(nodes1).thenReturn(nodes()); 96 | 97 | resolver.resolve(FQDN); 98 | 99 | assertThat(resolver.resolve(FQDN), equalTo(nodes1)); 100 | } 101 | 102 | @Test 103 | public void shouldRetainDataIfNewResultEmptyAsync() throws ExecutionException, InterruptedException { 104 | when(delegate.resolveAsync(FQDN)) 105 | .thenReturn(CompletableFuture.completedFuture(nodes1)) 106 | .thenReturn(CompletableFuture.completedFuture(nodes())); 107 | 108 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 109 | 110 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get(), equalTo(nodes1)); 111 | } 112 | 113 | @Test 114 | public void shouldRetainDataOnFailure() { 115 | when(delegate.resolve(FQDN)) 116 | .thenReturn(nodes1) 117 | .thenThrow(new DnsException("expected")); 118 | 119 | resolver.resolve(FQDN); 120 | 121 | assertThat(resolver.resolve(FQDN), equalTo(nodes1)); 122 | } 123 | 124 | @Test 125 | public void shouldRetainDataOnFailureAsync() throws ExecutionException, InterruptedException { 126 | when(delegate.resolveAsync(FQDN)) 127 | .thenReturn(CompletableFuture.completedFuture(nodes1)) 128 | .thenReturn(DnsTestUtil.failedFuture(new DnsException("expected"))); 129 | 130 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 131 | 132 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get(), equalTo(nodes1)); 133 | } 134 | 135 | @Test 136 | public void shouldThrowOnFailureAndNoDataAvailable() { 137 | when(delegate.resolve(FQDN)).thenThrow(new DnsException("expected")); 138 | 139 | thrown.expect(DnsException.class); 140 | thrown.expectMessage("expected"); 141 | 142 | resolver.resolve(FQDN); 143 | } 144 | 145 | @Test 146 | public void shouldThrowOnFailureAndNoDataAvailableAsync() throws ExecutionException, InterruptedException { 147 | DnsException cause = new DnsException("expected"); 148 | when(delegate.resolveAsync(FQDN)).thenReturn(DnsTestUtil.failedFuture(cause)); 149 | 150 | thrown.expect(ExecutionException.class); 151 | thrown.expectCause(is(cause)); 152 | 153 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 154 | } 155 | 156 | @Test 157 | public void shouldReturnEmptyOnEmptyAndNoDataAvailable() { 158 | when(delegate.resolve(FQDN)).thenReturn(nodes()); 159 | 160 | assertThat(resolver.resolve(FQDN).isEmpty(), is(true)); 161 | } 162 | 163 | @Test 164 | public void shouldReturnEmptyOnEmptyAndNoDataAvailableAsync() throws ExecutionException, InterruptedException { 165 | when(delegate.resolveAsync(FQDN)).thenReturn(CompletableFuture.completedFuture(nodes())); 166 | 167 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get().isEmpty(), is(true)); 168 | } 169 | 170 | @Test 171 | public void shouldNotStoreEmptyResults() { 172 | when(delegate.resolve(FQDN)) 173 | .thenReturn(nodes()) 174 | .thenThrow(new DnsException("expected")); 175 | 176 | resolver.resolve(FQDN); 177 | 178 | thrown.expect(DnsException.class); 179 | thrown.expectMessage("expected"); 180 | 181 | resolver.resolve(FQDN); 182 | } 183 | 184 | @Test 185 | public void shouldNotStoreEmptyResultsAsync() throws ExecutionException, InterruptedException { 186 | DnsException cause = new DnsException("expected"); 187 | when(delegate.resolveAsync(FQDN)) 188 | .thenReturn(CompletableFuture.completedFuture(nodes())) 189 | .thenReturn(DnsTestUtil.failedFuture(cause)); 190 | 191 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 192 | 193 | thrown.expect(ExecutionException.class); 194 | thrown.expectCause(is(cause)); 195 | 196 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 197 | } 198 | 199 | @Test 200 | public void shouldNotRetainPastEndOfRetentionOnEmptyResults() throws Exception { 201 | when(delegate.resolve(FQDN)) 202 | .thenReturn(nodes("aresult")) 203 | .thenReturn(nodes()); 204 | 205 | resolver.resolve(FQDN); 206 | 207 | // expire retained entry 208 | Thread.sleep(RETENTION_TIME_MILLIS); 209 | 210 | assertThat(resolver.resolve(FQDN).isEmpty(), is(true)); 211 | } 212 | 213 | @Test 214 | public void shouldNotRetainPastEndOfRetentionOnEmptyResultsAsync() throws Exception { 215 | when(delegate.resolveAsync(FQDN)) 216 | .thenReturn(CompletableFuture.completedFuture(nodes("aresult"))) 217 | .thenReturn(CompletableFuture.completedFuture(nodes())); 218 | 219 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 220 | 221 | // expire retained entry 222 | Thread.sleep(RETENTION_TIME_MILLIS); 223 | 224 | assertThat(resolver.resolveAsync(FQDN).toCompletableFuture().get().isEmpty(), is(true)); 225 | } 226 | 227 | @Test 228 | public void shouldNotRetainPastEndOfRetentionOnException() throws Exception { 229 | DnsException expected = new DnsException("expected"); 230 | when(delegate.resolve(FQDN)) 231 | .thenReturn(nodes("aresult")) 232 | .thenThrow(expected); 233 | 234 | resolver.resolve(FQDN); 235 | 236 | // expire retained entry 237 | Thread.sleep(RETENTION_TIME_MILLIS); 238 | 239 | thrown.expect(equalTo(expected)); 240 | 241 | resolver.resolve(FQDN); 242 | } 243 | 244 | @Test 245 | public void shouldNotRetainPastEndOfRetentionOnExceptionAsync() throws Exception { 246 | DnsException expected = new DnsException("expected"); 247 | when(delegate.resolveAsync(FQDN)) 248 | .thenReturn(CompletableFuture.completedFuture(nodes("aresult"))) 249 | .thenReturn(DnsTestUtil.failedFuture(expected)); 250 | 251 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 252 | 253 | // expire retained entry 254 | Thread.sleep(RETENTION_TIME_MILLIS); 255 | 256 | thrown.expectCause(equalTo(expected)); 257 | 258 | resolver.resolveAsync(FQDN).toCompletableFuture().get(); 259 | } 260 | 261 | @Test 262 | public void shouldThrowIfRetentionNegative() { 263 | thrown.expect(IllegalArgumentException.class); 264 | thrown.expectMessage("-4787"); 265 | 266 | new RetainingDnsSrvResolver(delegate, -4787); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/ServiceResolvingChangeNotifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.hamcrest.Matchers.containsInAnyOrder; 21 | import static org.junit.Assert.assertThat; 22 | import static org.junit.Assert.fail; 23 | import static org.mockito.Matchers.any; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.never; 26 | import static org.mockito.Mockito.times; 27 | import static org.mockito.Mockito.verify; 28 | import static org.mockito.Mockito.verifyNoMoreInteractions; 29 | import static org.mockito.Mockito.when; 30 | 31 | import java.util.Arrays; 32 | import java.util.List; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.function.Function; 36 | import org.junit.Before; 37 | import org.junit.Test; 38 | import org.mockito.ArgumentCaptor; 39 | import org.mockito.Mock; 40 | import org.mockito.MockitoAnnotations; 41 | 42 | public class ServiceResolvingChangeNotifierTest { 43 | 44 | private static final String FQDN = "example.com"; 45 | 46 | @Mock 47 | public DnsSrvResolver resolver; 48 | 49 | @Mock 50 | ErrorHandler errorHandler; 51 | 52 | @Before 53 | public void setUp() { 54 | MockitoAnnotations.initMocks(this); 55 | } 56 | 57 | @Test 58 | @SuppressWarnings("unchecked") 59 | public void shouldCallListenerOnChange() { 60 | ChangeNotifierFactory.RunnableChangeNotifier sut = createNotifier(); 61 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 62 | sut.setListener(listener, false); 63 | 64 | LookupResult result1 = result("host", 1234); 65 | LookupResult result2 = result("host", 4321); 66 | when(resolver.resolve(FQDN)).thenReturn(Arrays.asList(result1), Arrays.asList(result1, result2)); 67 | when(resolver.resolveAsync(FQDN)) 68 | .thenReturn(CompletableFuture.completedFuture(Arrays.asList(result1)), 69 | CompletableFuture.completedFuture(Arrays.asList(result1, result2))); 70 | 71 | sut.run(); 72 | sut.run(); 73 | 74 | ArgumentCaptor captor = 75 | ArgumentCaptor.forClass(ChangeNotifier.ChangeNotification.class); 76 | verify(listener, times(2)).onChange(captor.capture()); 77 | 78 | List notifications = captor.getAllValues(); 79 | assertThat(notifications.size(), is(2)); 80 | 81 | ChangeNotifier.ChangeNotification change1 = notifications.get(0); 82 | assertThat(change1.previous().size(), is(0)); 83 | assertThat(change1.current().size(), is(1)); 84 | assertThat(change1.current(), containsInAnyOrder(result1)); 85 | 86 | ChangeNotifier.ChangeNotification change2 = notifications.get(1); 87 | assertThat(change2.previous().size(), is(1)); 88 | assertThat(change2.previous(), containsInAnyOrder(result1)); 89 | assertThat(change2.current().size(), is(2)); 90 | assertThat(change2.current(), containsInAnyOrder(result1, result2)); 91 | } 92 | 93 | @Test 94 | @SuppressWarnings("unchecked") 95 | public void shouldCallListenerOnSet() { 96 | ChangeNotifierFactory.RunnableChangeNotifier sut = createNotifier(); 97 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 98 | 99 | LookupResult result = result("host", 1234); 100 | when(resolver.resolve(FQDN)) 101 | .thenReturn(Arrays.asList(result)); 102 | when(resolver.resolveAsync(FQDN)) 103 | .thenReturn(CompletableFuture.completedFuture(Arrays.asList(result))); 104 | 105 | sut.run(); 106 | sut.setListener(listener, true); 107 | 108 | ArgumentCaptor captor = 109 | ArgumentCaptor.forClass(ChangeNotifier.ChangeNotification.class); 110 | verify(listener).onChange(captor.capture()); 111 | 112 | ChangeNotifier.ChangeNotification notification = captor.getValue(); 113 | assertThat(notification.previous().size(), is(0)); 114 | assertThat(notification.current().size(), is(1)); 115 | assertThat(notification.current(), containsInAnyOrder(result)); 116 | } 117 | 118 | @Test 119 | @SuppressWarnings("unchecked") 120 | public void shouldReturnImmutableSets() { 121 | ChangeNotifierFactory.RunnableChangeNotifier sut = createNotifier(); 122 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 123 | 124 | LookupResult result1 = result("host", 1234); 125 | LookupResult result2 = result("host", 4321); 126 | when(resolver.resolve(FQDN)) 127 | .thenReturn(Arrays.asList(result1), Arrays.asList(result1, result2)); 128 | when(resolver.resolveAsync(FQDN)) 129 | .thenReturn(CompletableFuture.completedFuture(Arrays.asList(result1)), 130 | CompletableFuture.completedFuture(Arrays.asList(result1, result2))); 131 | 132 | sut.run(); 133 | sut.setListener(listener, true); 134 | sut.run(); 135 | 136 | ArgumentCaptor captor = 137 | ArgumentCaptor.forClass(ChangeNotifier.ChangeNotification.class); 138 | verify(listener, times(2)).onChange(captor.capture()); 139 | 140 | for (ChangeNotifier.ChangeNotification notification : captor.getAllValues()){ 141 | try { 142 | notification.previous().clear(); 143 | fail(); 144 | } catch (UnsupportedOperationException ignore) { 145 | } 146 | try { 147 | notification.current().clear(); 148 | fail(); 149 | } catch (UnsupportedOperationException ignore) { 150 | } 151 | } 152 | } 153 | 154 | @Test 155 | @SuppressWarnings("unchecked") 156 | public void shouldOnlyChangeIfTransformedValuesChange() { 157 | ChangeNotifierFactory.RunnableChangeNotifier sut = createHostNotifier(); 158 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 159 | sut.setListener(listener, false); 160 | 161 | LookupResult result1 = result("host", 1234); 162 | LookupResult result2 = result("host", 4321); 163 | when(resolver.resolve(FQDN)) 164 | .thenReturn(Arrays.asList(result1), Arrays.asList(result1, result2)); 165 | when(resolver.resolveAsync(FQDN)) 166 | .thenReturn(CompletableFuture.completedFuture(Arrays.asList(result1)), 167 | CompletableFuture.completedFuture(Arrays.asList(result1, result2))); 168 | 169 | sut.run(); 170 | sut.run(); 171 | 172 | ArgumentCaptor captor = 173 | ArgumentCaptor.forClass(ChangeNotifier.ChangeNotification.class); 174 | verify(listener).onChange(captor.capture()); 175 | 176 | ChangeNotifier.ChangeNotification notification = captor.getValue(); 177 | assertThat(notification.previous().size(), is(0)); 178 | assertThat(notification.current().size(), is(1)); 179 | assertThat(notification.current(), containsInAnyOrder("host")); 180 | } 181 | 182 | @Test 183 | @SuppressWarnings("unchecked") 184 | public void shouldStopResolvingAfterClose() { 185 | ChangeNotifierFactory.RunnableChangeNotifier sut = createNotifier(); 186 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 187 | sut.setListener(listener, false); 188 | 189 | sut.close(); 190 | sut.run(); 191 | 192 | verify(resolver, never()).resolve(any(String.class)); 193 | verify(listener, never()).onChange(any(ChangeNotifier.ChangeNotification.class)); 194 | } 195 | 196 | @Test 197 | @SuppressWarnings("unchecked") 198 | public void shouldDoSomethingWithNulls() { 199 | Function f = mock(Function.class); 200 | ChangeNotifierFactory.RunnableChangeNotifier sut = createTransformingNotifier(f); 201 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 202 | 203 | when(resolver.resolve(FQDN)) 204 | .thenReturn(Arrays.asList( 205 | result("host1", 1234), 206 | result("host2", 1234), 207 | result("host3", 1234))); 208 | when(resolver.resolveAsync(FQDN)) 209 | .thenReturn(CompletableFuture.completedFuture(Arrays.asList( 210 | result("host1", 1234), 211 | result("host2", 1234), 212 | result("host3", 1234)))); 213 | 214 | when(f.apply(any(LookupResult.class))) 215 | .thenReturn("foo", null, "bar"); 216 | 217 | sut.setListener(listener, false); 218 | sut.run(); 219 | 220 | verify(listener, times(1)).onChange(any(ChangeNotifier.ChangeNotification.class)); 221 | verifyNoMoreInteractions(listener); 222 | } 223 | 224 | @Test 225 | @SuppressWarnings("unchecked") 226 | public void shouldCallErrorHandlerOnResolveErrors() { 227 | Function f = mock(Function.class); 228 | ChangeNotifierFactory.RunnableChangeNotifier sut = createTransformingNotifier(f); 229 | ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); 230 | 231 | DnsException exception = new DnsException("something wrong"); 232 | when(resolver.resolve(FQDN)) 233 | .thenThrow(exception); 234 | when(resolver.resolveAsync(FQDN)) 235 | .thenReturn(DnsTestUtil.failedFuture(exception)); 236 | 237 | sut.setListener(listener, false); 238 | sut.run(); 239 | 240 | verify(errorHandler).handle(FQDN, exception); 241 | verifyNoMoreInteractions(f); 242 | verify(listener, times(1)).onChange(any(ChangeNotifier.ChangeNotification.class)); 243 | verifyNoMoreInteractions(listener); 244 | } 245 | 246 | private ChangeNotifierFactory.RunnableChangeNotifier createNotifier() { 247 | return createTransformingNotifier(Function.identity()); 248 | } 249 | 250 | private ChangeNotifierFactory.RunnableChangeNotifier createHostNotifier() { 251 | return createTransformingNotifier(input -> input != null ? input.host() : null); 252 | } 253 | 254 | private ChangeNotifierFactory.RunnableChangeNotifier createTransformingNotifier( 255 | Function f) { 256 | return new ServiceResolvingChangeNotifier(resolver, FQDN, f, errorHandler); 257 | } 258 | 259 | private static LookupResult result(String host, int port) { 260 | return LookupResult.create(host, port, 1, 5000, 300); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/SimpleLookupFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.hamcrest.CoreMatchers.isA; 21 | import static org.hamcrest.CoreMatchers.notNullValue; 22 | import static org.junit.Assert.assertThat; 23 | 24 | import org.junit.Before; 25 | import org.junit.Rule; 26 | import org.junit.Test; 27 | import org.junit.rules.ExpectedException; 28 | import org.xbill.DNS.Lookup; 29 | import org.xbill.DNS.TextParseException; 30 | import org.xbill.DNS.lookup.LookupSession; 31 | 32 | import java.util.concurrent.ForkJoinPool; 33 | 34 | 35 | public class SimpleLookupFactoryTest { 36 | SimpleLookupFactory factory; 37 | 38 | @Rule 39 | public ExpectedException thrown = ExpectedException.none(); 40 | 41 | @Before 42 | public void setUp() { 43 | factory = new SimpleLookupFactory(ForkJoinPool.commonPool()); 44 | } 45 | 46 | @Test 47 | public void shouldCreateLookups() { 48 | assertThat(factory.forName("some.domain."), is(notNullValue())); 49 | } 50 | 51 | @Test 52 | public void shouldCreateLookupSession() { 53 | assertThat(factory.sessionForName("some.domain."), is(notNullValue())); 54 | } 55 | 56 | @Test 57 | public void shouldNotCreateNewLookupsEachTime() { 58 | Lookup first = factory.forName("some.other.name."); 59 | Lookup second = factory.forName("some.other.name."); 60 | 61 | assertThat(first == second, is(false)); 62 | } 63 | 64 | @Test 65 | public void shouldCreateNewLookupSessionEachTime() { 66 | LookupSession firstSession = factory.sessionForName("some.other.name."); 67 | LookupSession secondSession = factory.sessionForName("some.other.name."); 68 | 69 | assertThat(firstSession == secondSession, is(true)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/XBillDnsSrvResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns; 18 | 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.hamcrest.Matchers.containsInAnyOrder; 21 | import static org.junit.Assert.assertThat; 22 | import static org.mockito.Matchers.any; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | import java.io.IOException; 27 | import java.util.List; 28 | import java.util.Set; 29 | import java.util.stream.Collectors; 30 | import org.junit.After; 31 | import org.junit.Before; 32 | import org.junit.Rule; 33 | import org.junit.Test; 34 | import org.junit.rules.ExpectedException; 35 | import org.xbill.DNS.DClass; 36 | import org.xbill.DNS.Lookup; 37 | import org.xbill.DNS.Message; 38 | import org.xbill.DNS.Name; 39 | import org.xbill.DNS.Rcode; 40 | import org.xbill.DNS.Record; 41 | import org.xbill.DNS.Resolver; 42 | import org.xbill.DNS.SRVRecord; 43 | import org.xbill.DNS.Section; 44 | import org.xbill.DNS.TextParseException; 45 | import org.xbill.DNS.Type; 46 | 47 | public class XBillDnsSrvResolverTest { 48 | XBillDnsSrvResolver resolver; 49 | 50 | LookupFactory lookupFactory; 51 | Resolver xbillResolver; 52 | 53 | @Rule public ExpectedException thrown = ExpectedException.none(); 54 | 55 | @Before 56 | public void setUp() { 57 | lookupFactory = mock(LookupFactory.class); 58 | 59 | resolver = new XBillDnsSrvResolver(lookupFactory); 60 | 61 | xbillResolver = mock(Resolver.class); 62 | } 63 | 64 | @After 65 | public void tearDown() { 66 | Lookup.refreshDefault(); 67 | } 68 | 69 | @Test 70 | public void shouldReturnResultsFromLookup() throws Exception { 71 | String fqdn = "thefqdn."; 72 | String[] resultNodes = new String[] { "node1.domain.", "node2.domain." }; 73 | 74 | setupResponseForQuery(fqdn, fqdn, resultNodes); 75 | 76 | List actual = resolver.resolve(fqdn); 77 | 78 | Set nodeNames = actual.stream().map(LookupResult::host).collect(Collectors.toSet()); 79 | 80 | assertThat(nodeNames, containsInAnyOrder(resultNodes)); 81 | } 82 | 83 | @Test 84 | public void shouldIndicateCauseFromXBillIfLookupFails() throws Exception { 85 | thrown.expect(DnsException.class); 86 | thrown.expectMessage("response does not match query"); 87 | 88 | String fqdn = "thefqdn."; 89 | setupResponseForQuery(fqdn, "somethingelse.", "node1.domain.", "node2.domain."); 90 | 91 | resolver.resolve(fqdn); 92 | } 93 | 94 | @Test 95 | public void shouldIndicateNameIfLookupFails() throws Exception { 96 | thrown.expect(DnsException.class); 97 | thrown.expectMessage("thefqdn."); 98 | 99 | String fqdn = "thefqdn."; 100 | setupResponseForQuery(fqdn, "somethingelse.", "node1.domain.", "node2.domain."); 101 | 102 | resolver.resolve(fqdn); 103 | } 104 | 105 | @Test 106 | public void shouldReturnEmptyForHostNotFound() throws Exception { 107 | String fqdn = "thefqdn."; 108 | 109 | when(lookupFactory.forName(fqdn)).thenReturn(testLookup(fqdn)); 110 | when(xbillResolver.send(any(Message.class))).thenReturn(messageWithRCode(fqdn, Rcode.NXDOMAIN)); 111 | 112 | assertThat(resolver.resolve(fqdn).isEmpty(), is(true)); 113 | } 114 | 115 | // not testing for type not found, as I don't know how to set that up... 116 | 117 | private Message messageWithRCode(String query, int rcode) throws TextParseException { 118 | Name queryName = Name.fromString(query); 119 | Record question = Record.newRecord(queryName, Type.SRV, DClass.IN); 120 | Message queryMessage = Message.newQuery(question); 121 | Message result = new Message(); 122 | result.setHeader(queryMessage.getHeader()); 123 | result.addRecord(question, Section.QUESTION); 124 | 125 | result.getHeader().setRcode(rcode); 126 | 127 | return result; 128 | } 129 | 130 | private void setupResponseForQuery(String queryFqdn, String responseFqdn, String... results) 131 | throws IOException { 132 | when(lookupFactory.forName(queryFqdn)).thenReturn(testLookup(queryFqdn)); 133 | when(xbillResolver.send(any(Message.class))) 134 | .thenReturn(messageWithNodes(responseFqdn, results)); 135 | } 136 | 137 | private Lookup testLookup(String thefqdn) throws TextParseException { 138 | Lookup result = new Lookup(thefqdn, Type.SRV); 139 | 140 | result.setResolver(xbillResolver); 141 | 142 | return result; 143 | } 144 | 145 | private Message messageWithNodes(String query, String[] names) throws TextParseException { 146 | Name queryName = Name.fromString(query); 147 | Record question = Record.newRecord(queryName, Type.SRV, DClass.IN); 148 | Message queryMessage = Message.newQuery(question); 149 | Message result = new Message(); 150 | result.setHeader(queryMessage.getHeader()); 151 | result.addRecord(question, Section.QUESTION); 152 | 153 | for (String name1 : names) { 154 | result.addRecord( 155 | new SRVRecord(queryName, DClass.IN, 1, 1, 1, 8080, Name.fromString(name1)), 156 | Section.ANSWER); 157 | } 158 | 159 | return result; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/examples/BasicUsage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns.examples; 18 | 19 | import com.spotify.dns.DnsSrvResolver; 20 | import com.spotify.dns.DnsSrvResolvers; 21 | import com.spotify.dns.LookupResult; 22 | import com.spotify.dns.statistics.DnsReporter; 23 | import com.spotify.dns.statistics.DnsTimingContext; 24 | import java.io.BufferedReader; 25 | import java.io.IOException; 26 | import java.io.InputStreamReader; 27 | 28 | public final class BasicUsage { 29 | 30 | private static final DnsReporter REPORTER = new StdoutReporter(); 31 | 32 | public static void main(String[] args) throws IOException { 33 | DnsSrvResolver resolver = DnsSrvResolvers.newBuilder() 34 | .cachingLookups(true) 35 | .retainingDataOnFailures(true) 36 | .metered(REPORTER) 37 | .dnsLookupTimeoutMillis(1000) 38 | .build(); 39 | 40 | boolean quit = false; 41 | BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 42 | 43 | while (!quit) { 44 | System.out.print("Enter a SRV name (empty to quit): "); 45 | String line = in.readLine(); 46 | 47 | if (line == null || line.isEmpty()) { 48 | quit = true; 49 | } else { 50 | resolver.resolveAsync(line).whenComplete((nodes, e) -> { 51 | if (e == null) { 52 | for (LookupResult node : nodes) { 53 | System.out.println(node); 54 | } 55 | } else { 56 | e.printStackTrace(System.out); 57 | } 58 | }); 59 | } 60 | } 61 | } 62 | 63 | public static class StdoutReporter implements DnsReporter { 64 | @Override 65 | public DnsTimingContext resolveTimer() { 66 | return new DnsTimingContext() { 67 | private final long start = System.currentTimeMillis(); 68 | 69 | @Override 70 | public void stop() { 71 | final long now = System.currentTimeMillis(); 72 | final long diff = now - start; 73 | System.out.println("Request took " + diff + "ms"); 74 | } 75 | }; 76 | } 77 | 78 | @Override 79 | public void reportFailure(Throwable error) { 80 | System.err.println("Error when resolving: " + error); 81 | error.printStackTrace(System.err); 82 | } 83 | 84 | @Override 85 | public void reportEmpty() { 86 | System.out.println("Empty response from server."); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/dns/examples/PollingUsage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.spotify.dns.examples; 18 | 19 | import com.google.common.collect.Sets; 20 | import com.spotify.dns.ChangeNotifier; 21 | import com.spotify.dns.DnsException; 22 | import com.spotify.dns.DnsSrvResolver; 23 | import com.spotify.dns.DnsSrvResolvers; 24 | import com.spotify.dns.DnsSrvWatcher; 25 | import com.spotify.dns.DnsSrvWatchers; 26 | import com.spotify.dns.ErrorHandler; 27 | import com.spotify.dns.LookupResult; 28 | import java.io.BufferedReader; 29 | import java.io.IOException; 30 | import java.io.InputStreamReader; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | public final class PollingUsage { 34 | 35 | public static void main(String[] args) throws IOException { 36 | DnsSrvResolver resolver = DnsSrvResolvers.newBuilder() 37 | .cachingLookups(true) 38 | .dnsLookupTimeoutMillis(1000) 39 | .build(); 40 | 41 | DnsSrvWatcher watcher = DnsSrvWatchers.newBuilder(resolver) 42 | .polling(1, TimeUnit.SECONDS) 43 | .withErrorHandler(new ErrorPrinter()) 44 | .build(); 45 | 46 | boolean quit = false; 47 | BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 48 | 49 | while (!quit) { 50 | System.out.print("Enter a SRV name (empty to quit): "); 51 | String line = in.readLine(); 52 | 53 | if (line == null || line.isEmpty()) { 54 | quit = true; 55 | } else { 56 | try { 57 | ChangeNotifier notifier = watcher.watch(line); 58 | notifier.setListener(new ChangeListener(line), false); 59 | } catch (DnsException e) { 60 | e.printStackTrace(System.out); 61 | } 62 | } 63 | } 64 | } 65 | 66 | static class ErrorPrinter implements ErrorHandler { 67 | 68 | @Override 69 | public void handle(String fqdn, DnsException exception) { 70 | System.out.println("Error with " + fqdn); 71 | exception.printStackTrace(); 72 | } 73 | } 74 | 75 | static class ChangeListener implements ChangeNotifier.Listener { 76 | 77 | final String name; 78 | 79 | ChangeListener(String name) { 80 | this.name = name; 81 | } 82 | 83 | @Override 84 | public void onChange(ChangeNotifier.ChangeNotification changeNotification) { 85 | System.out.println("\nEndpoints changed for " + name); 86 | for (LookupResult endpoint : changeNotification.previous()) { 87 | System.out.println(" prev: " + endpoint); 88 | } 89 | 90 | for (LookupResult endpoint : changeNotification.current()) { 91 | System.out.println(" curr: " + endpoint); 92 | } 93 | 94 | final Sets.SetView unchanged = 95 | Sets.intersection(changeNotification.current(), changeNotification.previous()); 96 | 97 | for (LookupResult endpoint : unchanged) { 98 | System.out.println(" noch: " + endpoint); 99 | } 100 | } 101 | } 102 | } 103 | --------------------------------------------------------------------------------