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 }