View Javadoc
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 &lt;major&gt;.&lt;minor&gt;.&lt;bugfix&gt;) 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 }