1 /* 2 * #%L 3 * ReadPreference.java - mongodb-async-driver - Allanbank Consulting, Inc. 4 * %% 5 * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc. 6 * %% 7 * Licensed under the Apache License, Version 2.0 (the "License"); 8 * you may not use this file except in compliance with the License. 9 * You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 * #L% 19 */ 20 package com.allanbank.mongodb; 21 22 import java.io.Serializable; 23 import java.util.ArrayList; 24 import java.util.Collections; 25 import java.util.Iterator; 26 import java.util.List; 27 28 import com.allanbank.mongodb.bson.Document; 29 import com.allanbank.mongodb.bson.DocumentAssignable; 30 import com.allanbank.mongodb.bson.Element; 31 import com.allanbank.mongodb.bson.NumericElement; 32 import com.allanbank.mongodb.bson.builder.ArrayBuilder; 33 import com.allanbank.mongodb.bson.builder.BuilderFactory; 34 import com.allanbank.mongodb.bson.builder.DocumentBuilder; 35 36 /** 37 * ReadPreference encapsulates a {@link Mode} and a set of tag matching 38 * documents. The {@link Mode} specified if primary and/or secondary servers in 39 * a replica set can be used and which should be tried first. The tag matching 40 * documents control which secondary servers can be used. 41 * <p> 42 * Each tag matching document specified the minimum set of key/values that the 43 * secondary server must be tagged with. As an example a tag matching document 44 * of <code>{ a : 1, b : 2 }</code> would match a server with the tags 45 * <code>{ a : 1, b : 2, c : 3 }</code> but would not match a server with tags 46 * <code>{ a : 1, c : 3, d : 4 }</code>. 47 * </p> 48 * <p> 49 * The tag matching documents and server tags must match exactly so a server 50 * with tags <code>{ a: true, b : 2 }</code> would not match the 51 * <code>{ a : 1, b : 2 }</code> tag matching document. Neither would a server 52 * with the tags <code>{ c: 1, b : 2 }</code>. 53 * </p> 54 * </p> Each tag matching document specifies a disjoint condition so a server 55 * has to match only one for the tag matching document. If the tag matching 56 * documents <code>{ a : 1, b : 2 }</code> and <code>{ a : 1, c : 3 }</code> are 57 * provided then both the server <code>{ a : 1, b : 2, c : 3 }</code> and 58 * <code>{ a : 1, c : 3, d : 4 }</code> would match. </p> 59 * <p> 60 * If no tag matching documents are specified then all secondary servers may be 61 * used. 62 * </p> 63 * 64 * @api.yes This class is part of the driver's API. Public and protected members 65 * will be deprecated for at least 1 non-bugfix release (version 66 * numbers are <major>.<minor>.<bugfix>) before being 67 * removed or modified. 68 * @copyright 2012-2013, Allanbank Consulting, Inc., All Rights Reserved 69 */ 70 public class ReadPreference implements Serializable, DocumentAssignable { 71 72 /** 73 * {@link ReadPreference} to read from the closest/{@link Mode#NEAREST} 74 * primary of secondary server. 75 */ 76 public static final ReadPreference CLOSEST = new ReadPreference( 77 Mode.NEAREST); 78 79 /** The name of the field in the query document for the read preferences. */ 80 public static final String FIELD_NAME = "$readPreference"; 81 82 /** 83 * {@link ReadPreference} to prefer reading from the primary but to fallback 84 * to a secondary if the primary is not available. 85 */ 86 public static final ReadPreference PREFER_PRIMARY = new ReadPreference( 87 Mode.PRIMARY_PREFERRED); 88 89 /** 90 * {@link ReadPreference} to prefer reading from a secondary but to 91 * 'fallback' to a primary if a secondary is not available. 92 */ 93 public static final ReadPreference PREFER_SECONDARY = new ReadPreference( 94 Mode.SECONDARY_PREFERRED); 95 96 /** The default {@link ReadPreference} to read from the primary only. */ 97 public static final ReadPreference PRIMARY = new ReadPreference( 98 Mode.PRIMARY_ONLY); 99 100 /** 101 * {@link ReadPreference} to read only from a secondary but using any 102 * secondary. 103 */ 104 public static final ReadPreference SECONDARY = new ReadPreference( 105 Mode.SECONDARY_ONLY); 106 107 /** The serialization version of the class. */ 108 private static final long serialVersionUID = -2135959854336511332L; 109 110 /** 111 * Creates a {@link ReadPreference} to read from the closest/ 112 * {@link Mode#NEAREST} primary of secondary server. 113 * <p> 114 * If tag matching documents are specified then only servers matching the 115 * specified tag matching documents would be used. 116 * </p> 117 * <p> 118 * If no tag matching documents are specified then returns {@link #CLOSEST}. 119 * </p> 120 * 121 * @param tagMatchDocuments 122 * Set of tag matching "documents" controlling which servers are 123 * used. 124 * @return The creates {@link ReadPreference}. 125 */ 126 public static ReadPreference closest( 127 final DocumentAssignable... tagMatchDocuments) { 128 if (tagMatchDocuments.length == 0) { 129 return CLOSEST; 130 } 131 return new ReadPreference(Mode.NEAREST, tagMatchDocuments); 132 } 133 134 /** 135 * Creates a {@link ReadPreference} to prefer reading from the primary but 136 * to fallback to a secondary if the primary is not available. 137 * <p> 138 * If tag matching documents are specified then only secondary servers 139 * matching the specified tag matching documents would be used. 140 * </p> 141 * <p> 142 * If no tag matching documents are specified then returns 143 * {@link #PREFER_PRIMARY}. 144 * </p> 145 * 146 * @param tagMatchDocuments 147 * Set of tag matching "documents" controlling which secondary 148 * servers are used. 149 * @return The creates {@link ReadPreference}. 150 */ 151 public static ReadPreference preferPrimary( 152 final DocumentAssignable... tagMatchDocuments) { 153 if (tagMatchDocuments.length == 0) { 154 return PREFER_PRIMARY; 155 } 156 return new ReadPreference(Mode.PRIMARY_PREFERRED, tagMatchDocuments); 157 } 158 159 /** 160 * Creates a {@link ReadPreference} to prefer reading from a secondary but 161 * to 'fallback' to a primary if a secondary is not available. 162 * <p> 163 * If tag matching documents are specified then only secondary servers 164 * matching the specified tag matching documents would be used. 165 * </p> 166 * <p> 167 * If no tag matching documents are specified then returns 168 * {@link #PREFER_SECONDARY}. 169 * </p> 170 * 171 * @param tagMatchDocuments 172 * Set of tag matching "documents" controlling which secondary 173 * servers are used. 174 * @return The creates {@link ReadPreference}. 175 */ 176 public static ReadPreference preferSecondary( 177 final DocumentAssignable... tagMatchDocuments) { 178 if (tagMatchDocuments.length == 0) { 179 return PREFER_SECONDARY; 180 } 181 return new ReadPreference(Mode.SECONDARY_PREFERRED, tagMatchDocuments); 182 } 183 184 /** 185 * Returns the default {@link ReadPreference} to read from the primary only: 186 * {@link #PRIMARY}. 187 * 188 * @return The {@link #PRIMARY} {@link ReadPreference}. 189 */ 190 public static ReadPreference primary() { 191 return PRIMARY; 192 } 193 194 /** 195 * Creates a {@link ReadPreference} to read only from a secondary. 196 * <p> 197 * If tag matching documents are specified then only secondary servers 198 * matching the specified tag matching documents would be used. 199 * </p> 200 * <p> 201 * If no tag matching documents are specified then returns 202 * {@link #PREFER_SECONDARY}. 203 * </p> 204 * 205 * @param tagMatchDocuments 206 * Set of tag matching "documents" controlling which secondary 207 * servers are used. 208 * @return The creates {@link ReadPreference}. 209 */ 210 public static ReadPreference secondary( 211 final DocumentAssignable... tagMatchDocuments) { 212 if (tagMatchDocuments.length == 0) { 213 return SECONDARY; 214 } 215 return new ReadPreference(Mode.SECONDARY_ONLY, tagMatchDocuments); 216 } 217 218 /** 219 * Creates a {@link ReadPreference} to read only from a specific server. 220 * <p> 221 * Used by the {@link MongoIterator} to ensure cursor fetch and terminate 222 * requests use the originating server. 223 * </p> 224 * <p> 225 * <b>Note:</b> Use this form of {@link ReadPreference} with caution. If the 226 * specified server fails all requests will fail. 227 * </p> 228 * 229 * @param address 230 * The server to read from. 231 * @return The creates {@link ReadPreference}. 232 */ 233 public static ReadPreference server(final String address) { 234 return new ReadPreference(Mode.SERVER, address); 235 } 236 237 /** The document form for the ReadPreference. */ 238 private final Document myDocumentForm; 239 240 /** 241 * The read preference mode controlling if primary or secondary servers can 242 * be used and which to prefer. 243 */ 244 private final Mode myMode; 245 246 /** 247 * The server to read from. Used by the {@link MongoIterator} to ensure 248 * cursor fetch and terminate requests use the originating server. 249 */ 250 private final String myServer; 251 252 /** The list of tag matching documents to control the secondaries used. */ 253 private final List<Document> myTagMatchingDocuments; 254 255 /** 256 * Creates a new ReadPreference. 257 * 258 * @param mode 259 * The read preference mode controlling if primary or secondary 260 * servers can be used and which to prefer. 261 * @param tagMatchDocuments 262 * Set of tag matching "documents" controlling which secondary 263 * servers are used. 264 */ 265 protected ReadPreference(final Mode mode, 266 final DocumentAssignable... tagMatchDocuments) { 267 final DocumentBuilder builder = BuilderFactory.start(); 268 builder.addString("mode", mode.getToken()); 269 270 myMode = mode; 271 myServer = null; 272 if (tagMatchDocuments.length == 0) { 273 myTagMatchingDocuments = Collections.emptyList(); 274 } 275 else { 276 myTagMatchingDocuments = new ArrayList<Document>( 277 tagMatchDocuments.length); 278 279 final ArrayBuilder tagsBuilder = builder.pushArray("tags"); 280 for (final DocumentAssignable assignable : tagMatchDocuments) { 281 final Document tags = assignable.asDocument(); 282 283 myTagMatchingDocuments.add(tags); 284 tagsBuilder.addDocument(tags); 285 } 286 } 287 288 myDocumentForm = builder.build(); 289 } 290 291 /** 292 * Creates a new ReadPreference. 293 * 294 * @param mode 295 * The read preference mode controlling if primary or secondary 296 * servers can be used and which to prefer. 297 * @param address 298 * The server to read from. 299 */ 300 protected ReadPreference(final Mode mode, final String address) { 301 myMode = mode; 302 myServer = address; 303 myTagMatchingDocuments = Collections.emptyList(); 304 305 final DocumentBuilder builder = BuilderFactory.start(); 306 builder.addString("mode", mode.getToken()); 307 if (address != null) { 308 builder.addString("server", address); 309 } 310 myDocumentForm = builder.build(); 311 312 } 313 314 /** 315 * {@inheritDoc} 316 * <p> 317 * Overridden to return the read preference document. 318 * </p> 319 * <p> 320 * The document contains: 321 * <ul> 322 * <li>The {@code mode} string value as returned by {@link Mode#getToken()}. 323 * </li> 324 * <li>An optional {@code tags} array containing the 325 * {@link #getTagMatchingDocuments() tag matching documents}</li> 326 * <li>An optional {@code server} containing the server to read from. This 327 * should only used for cursors to ensure the correct server is used to 328 * request more documents.</li> 329 * </ul> 330 * </p> 331 */ 332 @Override 333 public Document asDocument() { 334 return myDocumentForm; 335 } 336 337 /** 338 * Determines if the passed object is of this same type as this object and 339 * if so that its fields are equal. 340 * 341 * @param object 342 * The object to compare to. 343 * 344 * @see java.lang.Object#equals(java.lang.Object) 345 */ 346 @Override 347 public boolean equals(final Object object) { 348 boolean result = false; 349 if (this == object) { 350 result = true; 351 } 352 else if ((object != null) && (getClass() == object.getClass())) { 353 final ReadPreference other = (ReadPreference) object; 354 355 result = myMode.equals(other.myMode) 356 && myTagMatchingDocuments 357 .equals(other.myTagMatchingDocuments) 358 && nullSafeEquals(myServer, other.myServer); 359 } 360 return result; 361 } 362 363 /** 364 * Returns the read preference mode controlling if primary or secondary 365 * servers can be used and which to prefer. 366 * 367 * @return The read preference mode controlling if primary or secondary 368 * servers can be used and which to prefer. 369 */ 370 public Mode getMode() { 371 return myMode; 372 } 373 374 /** 375 * Returns the server to read from. Used by the {@link MongoIterator} to 376 * ensure cursor fetch and terminate requests use the originating server. 377 * 378 * @return The server to read from. 379 */ 380 public String getServer() { 381 return myServer; 382 } 383 384 /** 385 * Returns the list of tag matching documents to control the secondaries 386 * used. 387 * 388 * @return The list of tag matching documents to control the secondaries 389 * used. 390 */ 391 public List<Document> getTagMatchingDocuments() { 392 return myTagMatchingDocuments; 393 } 394 395 /** 396 * Computes a reasonable hash code. 397 * 398 * @return The hash code value. 399 */ 400 @Override 401 public int hashCode() { 402 int result = 1; 403 result = (31 * result) + myMode.ordinal(); 404 result = (31 * result) + myTagMatchingDocuments.hashCode(); 405 return result; 406 } 407 408 /** 409 * Returns true if the read preference is compatible with the legacy 410 * "slaveOk", e.g., is one of {@link Mode#PRIMARY_ONLY}, 411 * {@link Mode#SECONDARY_ONLY}, or {@link Mode#SERVER} and has no tag 412 * matching documents. 413 * 414 * @return True if the mode allows reading from secondaries, false 415 * otherwise. 416 */ 417 public boolean isLegacy() { 418 return ((myMode == Mode.PRIMARY_ONLY) 419 || (myMode == Mode.SECONDARY_ONLY) || (myMode == Mode.SERVER)) 420 && myTagMatchingDocuments.isEmpty(); 421 } 422 423 /** 424 * Returns true if the mode allows reading from secondaries, false 425 * otherwise. 426 * 427 * @return True if the mode allows reading from secondaries, false 428 * otherwise. 429 */ 430 public boolean isSecondaryOk() { 431 return myMode.isSecondaryOk(); 432 } 433 434 /** 435 * Returns true if this {@link ReadPreference} matches the <tt>tags</tt> 436 * document. 437 * 438 * @param tags 439 * The tags to be matched against. 440 * @return True if this {@link ReadPreference} matches the tags, false 441 * otherwise. 442 */ 443 public boolean matches(final Document tags) { 444 if (myTagMatchingDocuments.isEmpty()) { 445 return true; 446 } 447 448 boolean matches = false; 449 final Iterator<Document> tagMatchingDocIter = myTagMatchingDocuments 450 .iterator(); 451 while (tagMatchingDocIter.hasNext() && !matches) { 452 final Document tagMatchingDoc = tagMatchingDocIter.next(); 453 454 if (tags == null) { 455 if (!tagMatchingDoc.iterator().hasNext()) { 456 // Empty tag document matches all. 457 matches = true; 458 } 459 } 460 else { 461 matches = true; 462 final Iterator<Element> tagMatchingElemIter = tagMatchingDoc 463 .iterator(); 464 while (tagMatchingElemIter.hasNext() && matches) { 465 final Element tagMatchingElem = tagMatchingElemIter.next(); 466 final Element tag = tags.get(tagMatchingElem.getName()); 467 468 // Note tag may be null... 469 if (!fuzzyEquals(tagMatchingElem, tag)) { 470 matches = false; 471 } 472 } 473 } 474 } 475 476 return matches; 477 } 478 479 /** 480 * {@inheritDoc} 481 * <p> 482 * Overridden to return a string representation of the read preference.. 483 * </p> 484 */ 485 @Override 486 public String toString() { 487 final StringBuilder builder = new StringBuilder(); 488 489 builder.append(myMode.name()); 490 491 if (myServer != null) { 492 builder.append('['); 493 builder.append(myServer); 494 builder.append(']'); 495 } 496 else if (!myTagMatchingDocuments.isEmpty()) { 497 builder.append('['); 498 boolean first = true; 499 for (final Document tagDoc : myTagMatchingDocuments) { 500 if (!first) { 501 builder.append(", "); 502 } 503 first = false; 504 505 builder.append('{'); 506 boolean firstElem = true; 507 for (final Element element : tagDoc) { 508 if (!firstElem) { 509 builder.append(", "); 510 } 511 firstElem = false; 512 513 builder.append(element); 514 } 515 builder.append('}'); 516 } 517 builder.append(']'); 518 } 519 520 return builder.toString(); 521 } 522 523 /** 524 * Does a null safe equals comparison. 525 * 526 * @param rhs 527 * The right-hand-side of the comparison. 528 * @param lhs 529 * The left-hand-side of the comparison. 530 * @return True if the rhs equals the lhs. Note: nullSafeEquals(null, null) 531 * returns true. 532 */ 533 protected boolean nullSafeEquals(final Object rhs, final Object lhs) { 534 return (rhs == lhs) || ((rhs != null) && rhs.equals(lhs)); 535 } 536 537 /** 538 * Compares if the two elements are equals allowing numeric values type to 539 * not be a strict match but when casted as a long those values must still 540 * compare equal. 541 * 542 * @param lhs 543 * The first element to compare. May not be <code>null</code>. 544 * @param rhs 545 * The second element to compare. May be <code>null</code>. 546 * @return True if the two elements compare equal ignore two 547 * {@link NumericElement}s' specific type. 548 */ 549 private boolean fuzzyEquals(final Element lhs, final Element rhs) { 550 // Be fuzzy on the integer/long/double. 551 if ((rhs instanceof NumericElement) && (lhs instanceof NumericElement)) { 552 final long tagValue = ((NumericElement) rhs).getLongValue(); 553 final long tagMatchingValue = ((NumericElement) lhs).getLongValue(); 554 return (tagValue == tagMatchingValue); 555 } 556 557 // Otherwise exact match. 558 return lhs.equals(rhs); 559 } 560 561 /** 562 * Hook into serialization to replace <tt>this</tt> object with the local 563 * {@link #CLOSEST}, {@link #PREFER_PRIMARY}, {@link #PREFER_SECONDARY}, 564 * {@link #PRIMARY}, or {@link #SECONDARY} instance as appropriate. 565 * 566 * @return Either the {@link #CLOSEST}, {@link #PREFER_PRIMARY}, 567 * {@link #PREFER_SECONDARY}, {@link #PRIMARY}, or 568 * {@link #SECONDARY} instance if <tt>this</tt> instance equals one 569 * of those instances otherwise <tt>this</tt> instance. 570 */ 571 private Object readResolve() { 572 if (this.equals(CLOSEST)) { 573 return CLOSEST; 574 } 575 else if (this.equals(PREFER_PRIMARY)) { 576 return PREFER_PRIMARY; 577 } 578 else if (this.equals(PREFER_SECONDARY)) { 579 return PREFER_SECONDARY; 580 } 581 else if (this.equals(PRIMARY)) { 582 return PRIMARY; 583 } 584 else if (this.equals(SECONDARY)) { 585 return SECONDARY; 586 } 587 else { 588 return this; 589 } 590 } 591 592 /** 593 * Enumeration of the basic {@link ReadPreference} modes of service. 594 * 595 * @copyright 2012-2013, Allanbank Consulting, Inc., All Rights Reserved 596 */ 597 public static enum Mode { 598 /** 599 * Use the nearest (by latency measurement) member of the replica set: 600 * either primary or secondary servers are allowed. 601 * <p> 602 * If tag matching documents are specified then only server matching the 603 * specified tag matching documents would be used. 604 * </p> 605 */ 606 NEAREST("nearest"), 607 608 /** 609 * Reads should only be attempted from the primary member of the replica 610 * set. 611 */ 612 PRIMARY_ONLY("primary"), 613 614 /** 615 * Read from the primary but in the case of a fault may fallback to a 616 * secondary. 617 * <p> 618 * If tag matching documents are specified and a fallback to a secondary 619 * is required then only secondaries matching the specified tag matching 620 * documents would be used. 621 * </p> 622 */ 623 PRIMARY_PREFERRED("primaryPreferred"), 624 625 /** 626 * Do not attempt to read from the primary. 627 * <p> 628 * If tag matching documents are specified then only secondaries 629 * matching the specified tag matching documents would be used. 630 * </p> 631 */ 632 SECONDARY_ONLY("secondary"), 633 634 /** 635 * Try to first read from a secondary. If none are available "fallback" 636 * to the primary. 637 * <p> 638 * If tag matching documents are specified then only secondaries 639 * matching the specified tag matching documents would be used. 640 * </p> 641 */ 642 SECONDARY_PREFERRED("secondaryPreferred"), 643 644 /** 645 * Do not attempt to read from any server other than the one specified. 646 * Used by the {@link MongoIterator} to ensure cursor fetch and 647 * terminate requests use the originating server. 648 */ 649 SERVER("server"); 650 651 /** The token passed to the mongos server when in a shared environment. */ 652 private final String myToken; 653 654 /** 655 * Creates a new Mode. 656 * 657 * @param token 658 * The token passed to the mongos server when in a shared 659 * environment. 660 */ 661 private Mode(final String token) { 662 myToken = token; 663 } 664 665 /** 666 * Returns the token passed to the mongos server when in a shared 667 * environment. 668 * 669 * @return The token passed to the mongos server when in a shared 670 * environment. 671 */ 672 public String getToken() { 673 return myToken; 674 } 675 676 /** 677 * Returns true if the mode allows reading from secondaries, false 678 * otherwise. 679 * 680 * @return True if the mode allows reading from secondaries, false 681 * otherwise. 682 */ 683 public boolean isSecondaryOk() { 684 return (this != PRIMARY_ONLY); 685 } 686 } 687 }