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 }