blob: c6efad54a4a5500209428e46a96cea6e05de65d6 [file] [log] [blame]
Brian O'Connorabafb502014-12-02 22:26:20 -08001package org.onosproject.store.service.impl;
Madan Jampani08822c42014-11-04 17:17:46 -08002
Madan Jampania88d1f52014-11-14 16:45:24 -08003import static org.onlab.util.Tools.namedThreads;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -08004import static org.slf4j.LoggerFactory.getLogger;
Brian O'Connorabafb502014-12-02 22:26:20 -08005import static org.onosproject.store.service.impl.ClusterMessagingProtocol.DB_SERIALIZER;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -08006
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -08007import java.io.ByteArrayInputStream;
8import java.io.ByteArrayOutputStream;
Madan Jampani08822c42014-11-04 17:17:46 -08009import java.util.ArrayList;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -080010import java.util.Arrays;
Madan Jampani08822c42014-11-04 17:17:46 -080011import java.util.List;
12import java.util.Map;
Madan Jampani12390c12014-11-12 00:35:56 -080013import java.util.Set;
Madan Jampania88d1f52014-11-14 16:45:24 -080014import java.util.concurrent.ExecutorService;
15import java.util.concurrent.Executors;
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -080016import java.util.zip.DeflaterOutputStream;
17import java.util.zip.InflaterInputStream;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -080018
Madan Jampani08822c42014-11-04 17:17:46 -080019import net.kuujo.copycat.Command;
20import net.kuujo.copycat.Query;
21import net.kuujo.copycat.StateMachine;
22
Brian O'Connorabafb502014-12-02 22:26:20 -080023import org.onosproject.store.cluster.messaging.MessageSubject;
24import org.onosproject.store.service.BatchReadRequest;
25import org.onosproject.store.service.BatchWriteRequest;
26import org.onosproject.store.service.ReadRequest;
27import org.onosproject.store.service.ReadResult;
28import org.onosproject.store.service.ReadStatus;
29import org.onosproject.store.service.VersionedValue;
30import org.onosproject.store.service.WriteRequest;
31import org.onosproject.store.service.WriteResult;
32import org.onosproject.store.service.WriteStatus;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080033import org.slf4j.Logger;
Madan Jampani08822c42014-11-04 17:17:46 -080034
Madan Jampanif5d263b2014-11-13 10:04:40 -080035import com.google.common.base.MoreObjects;
Yuta HIGUCHI841c0b62014-11-13 20:27:14 -080036import com.google.common.collect.ImmutableMap;
Madan Jampanidef2c652014-11-12 13:50:10 -080037import com.google.common.collect.ImmutableSet;
Madan Jampani932c6ba2014-11-12 01:36:04 -080038import com.google.common.collect.Lists;
Madan Jampani08822c42014-11-04 17:17:46 -080039import com.google.common.collect.Maps;
Madan Jampanidef2c652014-11-12 13:50:10 -080040import com.google.common.collect.Sets;
Madan Jampani08822c42014-11-04 17:17:46 -080041
Madan Jampani686fa182014-11-04 23:16:27 -080042/**
43 * StateMachine whose transitions are coordinated/replicated
44 * by Raft consensus.
45 * Each Raft cluster member has a instance of this state machine that is
46 * independently updated in lock step once there is consensus
47 * on the next transition.
48 */
Madan Jampani08822c42014-11-04 17:17:46 -080049public class DatabaseStateMachine implements StateMachine {
50
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080051 private final Logger log = getLogger(getClass());
52
Madan Jampania88d1f52014-11-14 16:45:24 -080053 private final ExecutorService updatesExecutor =
54 Executors.newSingleThreadExecutor(namedThreads("database-statemachine-updates"));
55
Madan Jampani12390c12014-11-12 00:35:56 -080056 // message subject for database update notifications.
Madan Jampani44e6a542014-11-12 01:06:51 -080057 public static final MessageSubject DATABASE_UPDATE_EVENTS =
Madan Jampani12390c12014-11-12 00:35:56 -080058 new MessageSubject("database-update-events");
59
Madan Jampanidef2c652014-11-12 13:50:10 -080060 private final Set<DatabaseUpdateEventListener> listeners = Sets.newIdentityHashSet();
Madan Jampani12390c12014-11-12 00:35:56 -080061
62 // durable internal state of the database.
Madan Jampani08822c42014-11-04 17:17:46 -080063 private State state = new State();
64
Yuta HIGUCHI91768e32014-11-22 05:06:35 -080065 // TODO make this configurable
66 private boolean compressSnapshot = true;
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -080067
Madan Jampani08822c42014-11-04 17:17:46 -080068 @Command
69 public boolean createTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -080070 TableMetadata metadata = new TableMetadata(tableName);
71 return createTable(metadata);
Madan Jampani12390c12014-11-12 00:35:56 -080072 }
73
74 @Command
Madan Jampania88d1f52014-11-14 16:45:24 -080075 public boolean createTable(String tableName, Integer ttlMillis) {
Madan Jampanidef2c652014-11-12 13:50:10 -080076 TableMetadata metadata = new TableMetadata(tableName, ttlMillis);
77 return createTable(metadata);
78 }
79
80 private boolean createTable(TableMetadata metadata) {
81 Map<String, VersionedValue> existingTable = state.getTable(metadata.tableName());
82 if (existingTable != null) {
83 return false;
Madan Jampani12390c12014-11-12 00:35:56 -080084 }
Madan Jampanidef2c652014-11-12 13:50:10 -080085 state.createTable(metadata);
Madan Jampania88d1f52014-11-14 16:45:24 -080086
87 updatesExecutor.submit(new Runnable() {
88 @Override
89 public void run() {
90 for (DatabaseUpdateEventListener listener : listeners) {
91 listener.tableCreated(metadata);
92 }
93 }
94 });
95
Madan Jampanidef2c652014-11-12 13:50:10 -080096 return true;
Madan Jampani08822c42014-11-04 17:17:46 -080097 }
98
99 @Command
100 public boolean dropTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800101 if (state.removeTable(tableName)) {
Madan Jampania88d1f52014-11-14 16:45:24 -0800102
103 updatesExecutor.submit(new Runnable() {
104 @Override
105 public void run() {
106 for (DatabaseUpdateEventListener listener : listeners) {
107 listener.tableDeleted(tableName);
108 }
109 }
110 });
111
Madan Jampani12390c12014-11-12 00:35:56 -0800112 return true;
113 }
114 return false;
Madan Jampani08822c42014-11-04 17:17:46 -0800115 }
116
117 @Command
118 public boolean dropAllTables() {
Madan Jampanidef2c652014-11-12 13:50:10 -0800119 Set<String> tableNames = state.getTableNames();
120 state.removeAllTables();
Madan Jampania88d1f52014-11-14 16:45:24 -0800121
122 updatesExecutor.submit(new Runnable() {
123 @Override
124 public void run() {
125 for (DatabaseUpdateEventListener listener : listeners) {
126 for (String tableName : tableNames) {
127 listener.tableDeleted(tableName);
128 }
129 }
Madan Jampani12390c12014-11-12 00:35:56 -0800130 }
Madan Jampania88d1f52014-11-14 16:45:24 -0800131 });
132
Madan Jampani08822c42014-11-04 17:17:46 -0800133 return true;
134 }
135
136 @Query
Madan Jampanidef2c652014-11-12 13:50:10 -0800137 public Set<String> listTables() {
138 return ImmutableSet.copyOf(state.getTableNames());
Madan Jampani08822c42014-11-04 17:17:46 -0800139 }
140
141 @Query
Madan Jampani12390c12014-11-12 00:35:56 -0800142 public List<ReadResult> read(BatchReadRequest batchRequest) {
143 List<ReadResult> results = new ArrayList<>(batchRequest.batchSize());
144 for (ReadRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800145 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani08822c42014-11-04 17:17:46 -0800146 if (table == null) {
Madan Jampani12390c12014-11-12 00:35:56 -0800147 results.add(new ReadResult(ReadStatus.NO_SUCH_TABLE, request.tableName(), request.key(), null));
Madan Jampani08822c42014-11-04 17:17:46 -0800148 continue;
149 }
Yuta HIGUCHI3bd8cdc2014-11-05 19:11:44 -0800150 VersionedValue value = VersionedValue.copy(table.get(request.key()));
Madan Jampani12390c12014-11-12 00:35:56 -0800151 results.add(new ReadResult(ReadStatus.OK, request.tableName(), request.key(), value));
Madan Jampani08822c42014-11-04 17:17:46 -0800152 }
153 return results;
154 }
155
Yuta HIGUCHI841c0b62014-11-13 20:27:14 -0800156 @Query
157 public Map<String, VersionedValue> getAll(String tableName) {
158 return ImmutableMap.copyOf(state.getTable(tableName));
159 }
160
161
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800162 WriteStatus checkIfApplicable(WriteRequest request, VersionedValue value) {
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800163
164 switch (request.type()) {
165 case PUT:
Madan Jampani12390c12014-11-12 00:35:56 -0800166 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800167
168 case PUT_IF_ABSENT:
169 if (value == null) {
Madan Jampani12390c12014-11-12 00:35:56 -0800170 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800171 }
Madan Jampani12390c12014-11-12 00:35:56 -0800172 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800173
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800174 case PUT_IF_VALUE:
175 case REMOVE_IF_VALUE:
176 if (value != null && Arrays.equals(value.value(), request.oldValue())) {
Madan Jampani12390c12014-11-12 00:35:56 -0800177 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800178 }
Madan Jampani12390c12014-11-12 00:35:56 -0800179 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800180
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800181 case PUT_IF_VERSION:
182 case REMOVE_IF_VERSION:
183 if (value != null && request.previousVersion() == value.version()) {
Madan Jampani12390c12014-11-12 00:35:56 -0800184 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800185 }
Madan Jampani12390c12014-11-12 00:35:56 -0800186 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800187
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800188 case REMOVE:
Madan Jampani12390c12014-11-12 00:35:56 -0800189 return WriteStatus.OK;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800190
Yuta HIGUCHIc6b8f612014-11-06 19:04:13 -0800191 default:
192 break;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800193 }
194 log.error("Should never reach here {}", request);
Madan Jampani12390c12014-11-12 00:35:56 -0800195 return WriteStatus.ABORTED;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800196 }
197
Madan Jampani08822c42014-11-04 17:17:46 -0800198 @Command
Madan Jampani12390c12014-11-12 00:35:56 -0800199 public List<WriteResult> write(BatchWriteRequest batchRequest) {
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800200
201 // applicability check
Madan Jampani08822c42014-11-04 17:17:46 -0800202 boolean abort = false;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800203 List<WriteResult> results = new ArrayList<>(batchRequest.batchSize());
204
Madan Jampani12390c12014-11-12 00:35:56 -0800205 for (WriteRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800206 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani08822c42014-11-04 17:17:46 -0800207 if (table == null) {
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800208 results.add(new WriteResult(WriteStatus.NO_SUCH_TABLE, null));
Madan Jampani08822c42014-11-04 17:17:46 -0800209 abort = true;
210 continue;
211 }
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800212 final VersionedValue value = table.get(request.key());
Madan Jampani12390c12014-11-12 00:35:56 -0800213 WriteStatus result = checkIfApplicable(request, value);
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800214 results.add(new WriteResult(result, value));
Madan Jampani12390c12014-11-12 00:35:56 -0800215 if (result != WriteStatus.OK) {
Madan Jampani08822c42014-11-04 17:17:46 -0800216 abort = true;
Madan Jampani08822c42014-11-04 17:17:46 -0800217 }
Madan Jampani08822c42014-11-04 17:17:46 -0800218 }
219
Madan Jampani08822c42014-11-04 17:17:46 -0800220 if (abort) {
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800221 for (int i = 0; i < results.size(); ++i) {
222 if (results.get(i).status() == WriteStatus.OK) {
223 results.set(i, new WriteResult(WriteStatus.ABORTED, null));
Madan Jampani08822c42014-11-04 17:17:46 -0800224 }
225 }
226 return results;
227 }
228
Madan Jampani12390c12014-11-12 00:35:56 -0800229 List<TableModificationEvent> tableModificationEvents = Lists.newLinkedList();
230
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800231 // apply changes
Madan Jampani12390c12014-11-12 00:35:56 -0800232 for (WriteRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800233 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani12390c12014-11-12 00:35:56 -0800234
235 TableModificationEvent tableModificationEvent = null;
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800236 // FIXME: If this method could be called by multiple thread,
237 // synchronization scope is wrong.
238 // Whole function including applicability check needs to be protected.
239 // Confirm copycat's thread safety requirement for StateMachine
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800240 // TODO: If we need isolation, we need to block reads also
Madan Jampani08822c42014-11-04 17:17:46 -0800241 synchronized (table) {
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800242 switch (request.type()) {
243 case PUT:
244 case PUT_IF_ABSENT:
245 case PUT_IF_VALUE:
246 case PUT_IF_VERSION:
247 VersionedValue newValue = new VersionedValue(request.newValue(), state.nextVersion());
248 VersionedValue previousValue = table.put(request.key(), newValue);
Madan Jampani12390c12014-11-12 00:35:56 -0800249 WriteResult putResult = new WriteResult(WriteStatus.OK, previousValue);
250 results.add(putResult);
251 tableModificationEvent = (previousValue == null) ?
Madan Jampani9b37d572014-11-12 11:53:24 -0800252 TableModificationEvent.rowAdded(request.tableName(), request.key(), newValue) :
253 TableModificationEvent.rowUpdated(request.tableName(), request.key(), newValue);
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800254 break;
255
256 case REMOVE:
257 case REMOVE_IF_VALUE:
258 case REMOVE_IF_VERSION:
259 VersionedValue removedValue = table.remove(request.key());
Madan Jampani12390c12014-11-12 00:35:56 -0800260 WriteResult removeResult = new WriteResult(WriteStatus.OK, removedValue);
261 results.add(removeResult);
262 if (removedValue != null) {
263 tableModificationEvent =
Madan Jampani9b37d572014-11-12 11:53:24 -0800264 TableModificationEvent.rowDeleted(request.tableName(), request.key(), removedValue);
Madan Jampani12390c12014-11-12 00:35:56 -0800265 }
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800266 break;
267
268 default:
269 log.error("Invalid WriteRequest type {}", request.type());
270 break;
271 }
Madan Jampani08822c42014-11-04 17:17:46 -0800272 }
Madan Jampani12390c12014-11-12 00:35:56 -0800273
274 if (tableModificationEvent != null) {
275 tableModificationEvents.add(tableModificationEvent);
276 }
Madan Jampani08822c42014-11-04 17:17:46 -0800277 }
Madan Jampani12390c12014-11-12 00:35:56 -0800278
279 // notify listeners of table mod events.
Madan Jampania88d1f52014-11-14 16:45:24 -0800280
281 updatesExecutor.submit(new Runnable() {
282 @Override
283 public void run() {
284 for (DatabaseUpdateEventListener listener : listeners) {
285 for (TableModificationEvent tableModificationEvent : tableModificationEvents) {
286 log.trace("Publishing table modification event: {}", tableModificationEvent);
287 listener.tableModified(tableModificationEvent);
288 }
289 }
Madan Jampani12390c12014-11-12 00:35:56 -0800290 }
Madan Jampania88d1f52014-11-14 16:45:24 -0800291 });
Madan Jampani12390c12014-11-12 00:35:56 -0800292
Madan Jampani08822c42014-11-04 17:17:46 -0800293 return results;
294 }
295
Madan Jampanidef2c652014-11-12 13:50:10 -0800296 public static class State {
Madan Jampani08822c42014-11-04 17:17:46 -0800297
Madan Jampanidef2c652014-11-12 13:50:10 -0800298 private final Map<String, TableMetadata> tableMetadata = Maps.newHashMap();
299 private final Map<String, Map<String, VersionedValue>> tableData = Maps.newHashMap();
Madan Jampani08822c42014-11-04 17:17:46 -0800300 private long versionCounter = 1;
301
Yuta HIGUCHI4490a732014-11-18 20:20:30 -0800302 Map<String, VersionedValue> getTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800303 return tableData.get(tableName);
304 }
305
306 void createTable(TableMetadata metadata) {
307 tableMetadata.put(metadata.tableName, metadata);
308 tableData.put(metadata.tableName, Maps.newHashMap());
309 }
310
311 TableMetadata getTableMetadata(String tableName) {
312 return tableMetadata.get(tableName);
Madan Jampani08822c42014-11-04 17:17:46 -0800313 }
314
315 long nextVersion() {
316 return versionCounter++;
317 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800318
319 Set<String> getTableNames() {
320 return ImmutableSet.copyOf(tableMetadata.keySet());
321 }
322
323
324 boolean removeTable(String tableName) {
325 if (!tableMetadata.containsKey(tableName)) {
326 return false;
327 }
328 tableMetadata.remove(tableName);
329 tableData.remove(tableName);
330 return true;
331 }
332
333 void removeAllTables() {
334 tableMetadata.clear();
335 tableData.clear();
336 }
337 }
338
339 public static class TableMetadata {
340 private final String tableName;
341 private final boolean expireOldEntries;
342 private final int ttlMillis;
343
344 public TableMetadata(String tableName) {
345 this.tableName = tableName;
346 this.expireOldEntries = false;
347 this.ttlMillis = Integer.MAX_VALUE;
348
349 }
350
351 public TableMetadata(String tableName, int ttlMillis) {
352 this.tableName = tableName;
353 this.expireOldEntries = true;
354 this.ttlMillis = ttlMillis;
355 }
356
357 public String tableName() {
358 return tableName;
359 }
360
361 public boolean expireOldEntries() {
362 return expireOldEntries;
363 }
364
365 public int ttlMillis() {
366 return ttlMillis;
367 }
Madan Jampanif5d263b2014-11-13 10:04:40 -0800368
369 @Override
370 public String toString() {
371 return MoreObjects.toStringHelper(getClass())
372 .add("tableName", tableName)
373 .add("expireOldEntries", expireOldEntries)
374 .add("ttlMillis", ttlMillis)
375 .toString();
376 }
Madan Jampani08822c42014-11-04 17:17:46 -0800377 }
378
379 @Override
380 public byte[] takeSnapshot() {
381 try {
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800382 if (compressSnapshot) {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800383 byte[] input = DB_SERIALIZER.encode(state);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800384 ByteArrayOutputStream comp = new ByteArrayOutputStream(input.length);
385 DeflaterOutputStream compressor = new DeflaterOutputStream(comp);
386 compressor.write(input, 0, input.length);
387 compressor.close();
388 return comp.toByteArray();
389 } else {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800390 return DB_SERIALIZER.encode(state);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800391 }
Madan Jampani08822c42014-11-04 17:17:46 -0800392 } catch (Exception e) {
Madan Jampani778f7ad2014-11-05 22:46:15 -0800393 log.error("Failed to take snapshot", e);
394 throw new SnapshotException(e);
Madan Jampani08822c42014-11-04 17:17:46 -0800395 }
396 }
397
398 @Override
399 public void installSnapshot(byte[] data) {
400 try {
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800401 if (compressSnapshot) {
402 ByteArrayInputStream in = new ByteArrayInputStream(data);
403 InflaterInputStream decompressor = new InflaterInputStream(in);
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800404 this.state = DB_SERIALIZER.decode(decompressor);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800405 } else {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800406 this.state = DB_SERIALIZER.decode(data);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800407 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800408
Madan Jampania88d1f52014-11-14 16:45:24 -0800409 updatesExecutor.submit(new Runnable() {
410 @Override
411 public void run() {
412 for (DatabaseUpdateEventListener listener : listeners) {
413 listener.snapshotInstalled(state);
414 }
415 }
416 });
417
Madan Jampani08822c42014-11-04 17:17:46 -0800418 } catch (Exception e) {
Madan Jampani778f7ad2014-11-05 22:46:15 -0800419 log.error("Failed to install from snapshot", e);
420 throw new SnapshotException(e);
Madan Jampani08822c42014-11-04 17:17:46 -0800421 }
422 }
Madan Jampani12390c12014-11-12 00:35:56 -0800423
Madan Jampanidef2c652014-11-12 13:50:10 -0800424 /**
425 * Adds specified DatabaseUpdateEventListener.
426 * @param listener listener to add
427 */
Madan Jampani12390c12014-11-12 00:35:56 -0800428 public void addEventListener(DatabaseUpdateEventListener listener) {
429 listeners.add(listener);
430 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800431
432 /**
433 * Removes specified DatabaseUpdateEventListener.
434 * @param listener listener to remove
435 */
436 public void removeEventListener(DatabaseUpdateEventListener listener) {
437 listeners.remove(listener);
438 }
Madan Jampani08822c42014-11-04 17:17:46 -0800439}