1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.gecko.sync.repositories.uploaders;
6 
7 import android.support.annotation.CheckResult;
8 import android.support.annotation.NonNull;
9 import android.support.annotation.Nullable;
10 
11 import org.mozilla.gecko.background.common.log.Logger;
12 
13 import java.util.ArrayList;
14 import java.util.List;
15 
16 import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.TokenModifiedException;
17 import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedChangedUnexpectedly;
18 import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedDidNotChange;
19 
20 /**
21  * Keeps track of token, Last-Modified value and GUIDs of succeeded records.
22  */
23 /* @ThreadSafe */
24 public class BatchMeta extends BufferSizeTracker {
25     private static final String LOG_TAG = "BatchMeta";
26 
27     // Will be set once first payload upload succeeds. We don't expect this to change until we
28     // commit the batch, and which point it must change.
29     /* @GuardedBy("this") */ private Long lastModified;
30 
31     // Will be set once first payload upload succeeds. We don't expect this to ever change until
32     // a commit succeeds, at which point this gets set to null.
33     /* @GuardedBy("this") */ private String token;
34 
35     /* @GuardedBy("accessLock") */ private boolean isUnlimited = false;
36 
37     // Accessed by synchronously running threads.
38     /* @GuardedBy("accessLock") */ private final List<String> successRecordGuids = new ArrayList<>();
39 
40     /* @GuardedBy("accessLock") */ private boolean needsCommit = false;
41 
42     protected final Long collectionLastModified;
43 
BatchMeta(@onNull Object payloadLock, long maxBytes, long maxRecords, @Nullable Long collectionLastModified)44     public BatchMeta(@NonNull Object payloadLock, long maxBytes, long maxRecords, @Nullable Long collectionLastModified) {
45         super(payloadLock, maxBytes, maxRecords);
46         this.collectionLastModified = collectionLastModified;
47     }
48 
setIsUnlimited(boolean isUnlimited)49     protected void setIsUnlimited(boolean isUnlimited) {
50         synchronized (accessLock) {
51             this.isUnlimited = isUnlimited;
52         }
53     }
54 
55     @Override
canFit(long recordDeltaByteCount)56     protected boolean canFit(long recordDeltaByteCount) {
57         synchronized (accessLock) {
58             return isUnlimited || super.canFit(recordDeltaByteCount);
59         }
60     }
61 
62     @Override
63     @CheckResult
addAndEstimateIfFull(long recordDeltaByteCount)64     protected boolean addAndEstimateIfFull(long recordDeltaByteCount) {
65         synchronized (accessLock) {
66             needsCommit = true;
67             boolean isFull = super.addAndEstimateIfFull(recordDeltaByteCount);
68             return !isUnlimited && isFull;
69         }
70     }
71 
needToCommit()72     protected boolean needToCommit() {
73         synchronized (accessLock) {
74             return needsCommit;
75         }
76     }
77 
getToken()78     protected synchronized String getToken() {
79         return token;
80     }
81 
setToken(final String newToken, boolean isCommit)82     protected synchronized void setToken(final String newToken, boolean isCommit) throws TokenModifiedException {
83         // Set token once in a batching mode.
84         // In a non-batching mode, this.token and newToken will be null, and this is a no-op.
85         if (token == null) {
86             token = newToken;
87             return;
88         }
89 
90         // Sanity checks.
91         if (isCommit) {
92             // We expect token to be null when commit payload succeeds.
93             if (newToken != null) {
94                 throw new TokenModifiedException();
95             } else {
96                 token = null;
97             }
98             return;
99         }
100 
101         // We expect new token to always equal current token for non-commit payloads.
102         if (!token.equals(newToken)) {
103             throw new TokenModifiedException();
104         }
105     }
106 
getLastModified()107     protected synchronized Long getLastModified() {
108         if (lastModified == null) {
109             return collectionLastModified;
110         }
111         return lastModified;
112     }
113 
setLastModified(final Long newLastModified, final boolean expectedToChange)114     protected synchronized void setLastModified(final Long newLastModified, final boolean expectedToChange) throws LastModifiedChangedUnexpectedly, LastModifiedDidNotChange {
115         if (lastModified == null) {
116             lastModified = newLastModified;
117             return;
118         }
119 
120         if (!expectedToChange && !lastModified.equals(newLastModified)) {
121             Logger.debug(LOG_TAG, "Last-Modified timestamp changed when we didn't expect it");
122             throw new LastModifiedChangedUnexpectedly();
123 
124         } else if (expectedToChange && lastModified.equals(newLastModified)) {
125             Logger.debug(LOG_TAG, "Last-Modified timestamp did not change when we expected it to");
126             throw new LastModifiedDidNotChange();
127 
128         } else {
129             lastModified = newLastModified;
130         }
131     }
132 
getSuccessRecordGuids()133     protected ArrayList<String> getSuccessRecordGuids() {
134         synchronized (accessLock) {
135             return new ArrayList<>(this.successRecordGuids);
136         }
137     }
138 
recordSucceeded(final String recordGuid)139     protected void recordSucceeded(final String recordGuid) {
140         // Sanity check.
141         if (recordGuid == null) {
142             throw new IllegalStateException();
143         }
144 
145         synchronized (accessLock) {
146             successRecordGuids.add(recordGuid);
147         }
148     }
149 
150     @Override
canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount)151     protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) {
152         return isUnlimited || super.canFitRecordByteDelta(byteDelta, recordCount, byteCount);
153     }
154 
155     @Override
reset()156     protected void reset() {
157         synchronized (accessLock) {
158             super.reset();
159             token = null;
160             lastModified = null;
161             successRecordGuids.clear();
162             needsCommit = false;
163         }
164     }
165 }