blob: b3cdf62541bd8e815035651f2b1ca1b5e667b226 [file] [log] [blame]
alshabibab984662014-12-04 18:56:18 -08001/*
2 * Copyright 2014 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Brian O'Connorabafb502014-12-02 22:26:20 -080016package org.onosproject.store.service.impl;
Madan Jampani08822c42014-11-04 17:17:46 -080017
Madan Jampania88d1f52014-11-14 16:45:24 -080018import static org.onlab.util.Tools.namedThreads;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080019import static org.slf4j.LoggerFactory.getLogger;
Brian O'Connorabafb502014-12-02 22:26:20 -080020import static org.onosproject.store.service.impl.ClusterMessagingProtocol.DB_SERIALIZER;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080021
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -080022import java.io.ByteArrayInputStream;
23import java.io.ByteArrayOutputStream;
Madan Jampani08822c42014-11-04 17:17:46 -080024import java.util.ArrayList;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -080025import java.util.Arrays;
Madan Jampani08822c42014-11-04 17:17:46 -080026import java.util.List;
27import java.util.Map;
Madan Jampani12390c12014-11-12 00:35:56 -080028import java.util.Set;
Madan Jampania88d1f52014-11-14 16:45:24 -080029import java.util.concurrent.ExecutorService;
30import java.util.concurrent.Executors;
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -080031import java.util.zip.DeflaterOutputStream;
32import java.util.zip.InflaterInputStream;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -080033
Madan Jampani08822c42014-11-04 17:17:46 -080034import net.kuujo.copycat.Command;
35import net.kuujo.copycat.Query;
36import net.kuujo.copycat.StateMachine;
37
Brian O'Connorabafb502014-12-02 22:26:20 -080038import org.onosproject.store.cluster.messaging.MessageSubject;
39import org.onosproject.store.service.BatchReadRequest;
40import org.onosproject.store.service.BatchWriteRequest;
41import org.onosproject.store.service.ReadRequest;
42import org.onosproject.store.service.ReadResult;
43import org.onosproject.store.service.ReadStatus;
44import org.onosproject.store.service.VersionedValue;
45import org.onosproject.store.service.WriteRequest;
46import org.onosproject.store.service.WriteResult;
47import org.onosproject.store.service.WriteStatus;
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080048import org.slf4j.Logger;
Madan Jampani08822c42014-11-04 17:17:46 -080049
Madan Jampanif5d263b2014-11-13 10:04:40 -080050import com.google.common.base.MoreObjects;
Yuta HIGUCHI841c0b62014-11-13 20:27:14 -080051import com.google.common.collect.ImmutableMap;
Madan Jampanidef2c652014-11-12 13:50:10 -080052import com.google.common.collect.ImmutableSet;
Madan Jampani932c6ba2014-11-12 01:36:04 -080053import com.google.common.collect.Lists;
Madan Jampani08822c42014-11-04 17:17:46 -080054import com.google.common.collect.Maps;
Madan Jampanidef2c652014-11-12 13:50:10 -080055import com.google.common.collect.Sets;
Madan Jampani08822c42014-11-04 17:17:46 -080056
Madan Jampani686fa182014-11-04 23:16:27 -080057/**
58 * StateMachine whose transitions are coordinated/replicated
59 * by Raft consensus.
60 * Each Raft cluster member has a instance of this state machine that is
61 * independently updated in lock step once there is consensus
62 * on the next transition.
63 */
Madan Jampani08822c42014-11-04 17:17:46 -080064public class DatabaseStateMachine implements StateMachine {
65
Yuta HIGUCHI39ae5502014-11-05 16:42:12 -080066 private final Logger log = getLogger(getClass());
67
Madan Jampania88d1f52014-11-14 16:45:24 -080068 private final ExecutorService updatesExecutor =
69 Executors.newSingleThreadExecutor(namedThreads("database-statemachine-updates"));
70
Madan Jampani12390c12014-11-12 00:35:56 -080071 // message subject for database update notifications.
Madan Jampani44e6a542014-11-12 01:06:51 -080072 public static final MessageSubject DATABASE_UPDATE_EVENTS =
Madan Jampani12390c12014-11-12 00:35:56 -080073 new MessageSubject("database-update-events");
74
Madan Jampanidef2c652014-11-12 13:50:10 -080075 private final Set<DatabaseUpdateEventListener> listeners = Sets.newIdentityHashSet();
Madan Jampani12390c12014-11-12 00:35:56 -080076
77 // durable internal state of the database.
Madan Jampani08822c42014-11-04 17:17:46 -080078 private State state = new State();
79
Yuta HIGUCHI91768e32014-11-22 05:06:35 -080080 // TODO make this configurable
81 private boolean compressSnapshot = true;
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -080082
Madan Jampani08822c42014-11-04 17:17:46 -080083 @Command
84 public boolean createTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -080085 TableMetadata metadata = new TableMetadata(tableName);
86 return createTable(metadata);
Madan Jampani12390c12014-11-12 00:35:56 -080087 }
88
89 @Command
Madan Jampania88d1f52014-11-14 16:45:24 -080090 public boolean createTable(String tableName, Integer ttlMillis) {
Madan Jampanidef2c652014-11-12 13:50:10 -080091 TableMetadata metadata = new TableMetadata(tableName, ttlMillis);
92 return createTable(metadata);
93 }
94
95 private boolean createTable(TableMetadata metadata) {
96 Map<String, VersionedValue> existingTable = state.getTable(metadata.tableName());
97 if (existingTable != null) {
98 return false;
Madan Jampani12390c12014-11-12 00:35:56 -080099 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800100 state.createTable(metadata);
Madan Jampania88d1f52014-11-14 16:45:24 -0800101
102 updatesExecutor.submit(new Runnable() {
103 @Override
104 public void run() {
105 for (DatabaseUpdateEventListener listener : listeners) {
106 listener.tableCreated(metadata);
107 }
108 }
109 });
110
Madan Jampanidef2c652014-11-12 13:50:10 -0800111 return true;
Madan Jampani08822c42014-11-04 17:17:46 -0800112 }
113
114 @Command
115 public boolean dropTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800116 if (state.removeTable(tableName)) {
Madan Jampania88d1f52014-11-14 16:45:24 -0800117
118 updatesExecutor.submit(new Runnable() {
119 @Override
120 public void run() {
121 for (DatabaseUpdateEventListener listener : listeners) {
122 listener.tableDeleted(tableName);
123 }
124 }
125 });
126
Madan Jampani12390c12014-11-12 00:35:56 -0800127 return true;
128 }
129 return false;
Madan Jampani08822c42014-11-04 17:17:46 -0800130 }
131
132 @Command
133 public boolean dropAllTables() {
Madan Jampanidef2c652014-11-12 13:50:10 -0800134 Set<String> tableNames = state.getTableNames();
135 state.removeAllTables();
Madan Jampania88d1f52014-11-14 16:45:24 -0800136
137 updatesExecutor.submit(new Runnable() {
138 @Override
139 public void run() {
140 for (DatabaseUpdateEventListener listener : listeners) {
141 for (String tableName : tableNames) {
142 listener.tableDeleted(tableName);
143 }
144 }
Madan Jampani12390c12014-11-12 00:35:56 -0800145 }
Madan Jampania88d1f52014-11-14 16:45:24 -0800146 });
147
Madan Jampani08822c42014-11-04 17:17:46 -0800148 return true;
149 }
150
151 @Query
Madan Jampanidef2c652014-11-12 13:50:10 -0800152 public Set<String> listTables() {
153 return ImmutableSet.copyOf(state.getTableNames());
Madan Jampani08822c42014-11-04 17:17:46 -0800154 }
155
156 @Query
Madan Jampani12390c12014-11-12 00:35:56 -0800157 public List<ReadResult> read(BatchReadRequest batchRequest) {
158 List<ReadResult> results = new ArrayList<>(batchRequest.batchSize());
159 for (ReadRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800160 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani08822c42014-11-04 17:17:46 -0800161 if (table == null) {
Madan Jampani12390c12014-11-12 00:35:56 -0800162 results.add(new ReadResult(ReadStatus.NO_SUCH_TABLE, request.tableName(), request.key(), null));
Madan Jampani08822c42014-11-04 17:17:46 -0800163 continue;
164 }
Yuta HIGUCHI3bd8cdc2014-11-05 19:11:44 -0800165 VersionedValue value = VersionedValue.copy(table.get(request.key()));
Madan Jampani12390c12014-11-12 00:35:56 -0800166 results.add(new ReadResult(ReadStatus.OK, request.tableName(), request.key(), value));
Madan Jampani08822c42014-11-04 17:17:46 -0800167 }
168 return results;
169 }
170
Yuta HIGUCHI841c0b62014-11-13 20:27:14 -0800171 @Query
172 public Map<String, VersionedValue> getAll(String tableName) {
173 return ImmutableMap.copyOf(state.getTable(tableName));
174 }
175
176
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800177 WriteStatus checkIfApplicable(WriteRequest request, VersionedValue value) {
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800178
179 switch (request.type()) {
180 case PUT:
Madan Jampani12390c12014-11-12 00:35:56 -0800181 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800182
183 case PUT_IF_ABSENT:
184 if (value == null) {
Madan Jampani12390c12014-11-12 00:35:56 -0800185 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800186 }
Madan Jampani12390c12014-11-12 00:35:56 -0800187 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800188
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800189 case PUT_IF_VALUE:
190 case REMOVE_IF_VALUE:
191 if (value != null && Arrays.equals(value.value(), request.oldValue())) {
Madan Jampani12390c12014-11-12 00:35:56 -0800192 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800193 }
Madan Jampani12390c12014-11-12 00:35:56 -0800194 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800195
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800196 case PUT_IF_VERSION:
197 case REMOVE_IF_VERSION:
198 if (value != null && request.previousVersion() == value.version()) {
Madan Jampani12390c12014-11-12 00:35:56 -0800199 return WriteStatus.OK;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800200 }
Madan Jampani12390c12014-11-12 00:35:56 -0800201 return WriteStatus.PRECONDITION_VIOLATION;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800202
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800203 case REMOVE:
Madan Jampani12390c12014-11-12 00:35:56 -0800204 return WriteStatus.OK;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800205
Yuta HIGUCHIc6b8f612014-11-06 19:04:13 -0800206 default:
207 break;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800208 }
209 log.error("Should never reach here {}", request);
Madan Jampani12390c12014-11-12 00:35:56 -0800210 return WriteStatus.ABORTED;
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800211 }
212
Madan Jampani08822c42014-11-04 17:17:46 -0800213 @Command
Madan Jampani12390c12014-11-12 00:35:56 -0800214 public List<WriteResult> write(BatchWriteRequest batchRequest) {
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800215
216 // applicability check
Madan Jampani08822c42014-11-04 17:17:46 -0800217 boolean abort = false;
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800218 List<WriteResult> results = new ArrayList<>(batchRequest.batchSize());
219
Madan Jampani12390c12014-11-12 00:35:56 -0800220 for (WriteRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800221 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani08822c42014-11-04 17:17:46 -0800222 if (table == null) {
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800223 results.add(new WriteResult(WriteStatus.NO_SUCH_TABLE, null));
Madan Jampani08822c42014-11-04 17:17:46 -0800224 abort = true;
225 continue;
226 }
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800227 final VersionedValue value = table.get(request.key());
Madan Jampani12390c12014-11-12 00:35:56 -0800228 WriteStatus result = checkIfApplicable(request, value);
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800229 results.add(new WriteResult(result, value));
Madan Jampani12390c12014-11-12 00:35:56 -0800230 if (result != WriteStatus.OK) {
Madan Jampani08822c42014-11-04 17:17:46 -0800231 abort = true;
Madan Jampani08822c42014-11-04 17:17:46 -0800232 }
Madan Jampani08822c42014-11-04 17:17:46 -0800233 }
234
Madan Jampani08822c42014-11-04 17:17:46 -0800235 if (abort) {
Yuta HIGUCHIfd0db482014-11-14 16:03:26 -0800236 for (int i = 0; i < results.size(); ++i) {
237 if (results.get(i).status() == WriteStatus.OK) {
238 results.set(i, new WriteResult(WriteStatus.ABORTED, null));
Madan Jampani08822c42014-11-04 17:17:46 -0800239 }
240 }
241 return results;
242 }
243
Madan Jampani12390c12014-11-12 00:35:56 -0800244 List<TableModificationEvent> tableModificationEvents = Lists.newLinkedList();
245
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800246 // apply changes
Madan Jampani12390c12014-11-12 00:35:56 -0800247 for (WriteRequest request : batchRequest.getAsList()) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800248 Map<String, VersionedValue> table = state.getTable(request.tableName());
Madan Jampani12390c12014-11-12 00:35:56 -0800249
250 TableModificationEvent tableModificationEvent = null;
Yuta HIGUCHIbddc81c42014-11-05 18:53:09 -0800251 // FIXME: If this method could be called by multiple thread,
252 // synchronization scope is wrong.
253 // Whole function including applicability check needs to be protected.
254 // Confirm copycat's thread safety requirement for StateMachine
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800255 // TODO: If we need isolation, we need to block reads also
Madan Jampani08822c42014-11-04 17:17:46 -0800256 synchronized (table) {
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800257 switch (request.type()) {
258 case PUT:
259 case PUT_IF_ABSENT:
260 case PUT_IF_VALUE:
261 case PUT_IF_VERSION:
262 VersionedValue newValue = new VersionedValue(request.newValue(), state.nextVersion());
263 VersionedValue previousValue = table.put(request.key(), newValue);
Madan Jampani12390c12014-11-12 00:35:56 -0800264 WriteResult putResult = new WriteResult(WriteStatus.OK, previousValue);
265 results.add(putResult);
266 tableModificationEvent = (previousValue == null) ?
Madan Jampani9b37d572014-11-12 11:53:24 -0800267 TableModificationEvent.rowAdded(request.tableName(), request.key(), newValue) :
268 TableModificationEvent.rowUpdated(request.tableName(), request.key(), newValue);
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800269 break;
270
271 case REMOVE:
272 case REMOVE_IF_VALUE:
273 case REMOVE_IF_VERSION:
274 VersionedValue removedValue = table.remove(request.key());
Madan Jampani12390c12014-11-12 00:35:56 -0800275 WriteResult removeResult = new WriteResult(WriteStatus.OK, removedValue);
276 results.add(removeResult);
277 if (removedValue != null) {
278 tableModificationEvent =
Madan Jampani9b37d572014-11-12 11:53:24 -0800279 TableModificationEvent.rowDeleted(request.tableName(), request.key(), removedValue);
Madan Jampani12390c12014-11-12 00:35:56 -0800280 }
Yuta HIGUCHI361664e2014-11-06 17:28:47 -0800281 break;
282
283 default:
284 log.error("Invalid WriteRequest type {}", request.type());
285 break;
286 }
Madan Jampani08822c42014-11-04 17:17:46 -0800287 }
Madan Jampani12390c12014-11-12 00:35:56 -0800288
289 if (tableModificationEvent != null) {
290 tableModificationEvents.add(tableModificationEvent);
291 }
Madan Jampani08822c42014-11-04 17:17:46 -0800292 }
Madan Jampani12390c12014-11-12 00:35:56 -0800293
294 // notify listeners of table mod events.
Madan Jampania88d1f52014-11-14 16:45:24 -0800295
296 updatesExecutor.submit(new Runnable() {
297 @Override
298 public void run() {
299 for (DatabaseUpdateEventListener listener : listeners) {
300 for (TableModificationEvent tableModificationEvent : tableModificationEvents) {
301 log.trace("Publishing table modification event: {}", tableModificationEvent);
302 listener.tableModified(tableModificationEvent);
303 }
304 }
Madan Jampani12390c12014-11-12 00:35:56 -0800305 }
Madan Jampania88d1f52014-11-14 16:45:24 -0800306 });
Madan Jampani12390c12014-11-12 00:35:56 -0800307
Madan Jampani08822c42014-11-04 17:17:46 -0800308 return results;
309 }
310
Madan Jampanidef2c652014-11-12 13:50:10 -0800311 public static class State {
Madan Jampani08822c42014-11-04 17:17:46 -0800312
Madan Jampanidef2c652014-11-12 13:50:10 -0800313 private final Map<String, TableMetadata> tableMetadata = Maps.newHashMap();
314 private final Map<String, Map<String, VersionedValue>> tableData = Maps.newHashMap();
Madan Jampani08822c42014-11-04 17:17:46 -0800315 private long versionCounter = 1;
316
Yuta HIGUCHI4490a732014-11-18 20:20:30 -0800317 Map<String, VersionedValue> getTable(String tableName) {
Madan Jampanidef2c652014-11-12 13:50:10 -0800318 return tableData.get(tableName);
319 }
320
321 void createTable(TableMetadata metadata) {
322 tableMetadata.put(metadata.tableName, metadata);
323 tableData.put(metadata.tableName, Maps.newHashMap());
324 }
325
326 TableMetadata getTableMetadata(String tableName) {
327 return tableMetadata.get(tableName);
Madan Jampani08822c42014-11-04 17:17:46 -0800328 }
329
330 long nextVersion() {
331 return versionCounter++;
332 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800333
334 Set<String> getTableNames() {
335 return ImmutableSet.copyOf(tableMetadata.keySet());
336 }
337
338
339 boolean removeTable(String tableName) {
340 if (!tableMetadata.containsKey(tableName)) {
341 return false;
342 }
343 tableMetadata.remove(tableName);
344 tableData.remove(tableName);
345 return true;
346 }
347
348 void removeAllTables() {
349 tableMetadata.clear();
350 tableData.clear();
351 }
352 }
353
354 public static class TableMetadata {
355 private final String tableName;
356 private final boolean expireOldEntries;
357 private final int ttlMillis;
358
359 public TableMetadata(String tableName) {
360 this.tableName = tableName;
361 this.expireOldEntries = false;
362 this.ttlMillis = Integer.MAX_VALUE;
363
364 }
365
366 public TableMetadata(String tableName, int ttlMillis) {
367 this.tableName = tableName;
368 this.expireOldEntries = true;
369 this.ttlMillis = ttlMillis;
370 }
371
372 public String tableName() {
373 return tableName;
374 }
375
376 public boolean expireOldEntries() {
377 return expireOldEntries;
378 }
379
380 public int ttlMillis() {
381 return ttlMillis;
382 }
Madan Jampanif5d263b2014-11-13 10:04:40 -0800383
384 @Override
385 public String toString() {
386 return MoreObjects.toStringHelper(getClass())
387 .add("tableName", tableName)
388 .add("expireOldEntries", expireOldEntries)
389 .add("ttlMillis", ttlMillis)
390 .toString();
391 }
Madan Jampani08822c42014-11-04 17:17:46 -0800392 }
393
394 @Override
395 public byte[] takeSnapshot() {
396 try {
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800397 if (compressSnapshot) {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800398 byte[] input = DB_SERIALIZER.encode(state);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800399 ByteArrayOutputStream comp = new ByteArrayOutputStream(input.length);
400 DeflaterOutputStream compressor = new DeflaterOutputStream(comp);
401 compressor.write(input, 0, input.length);
402 compressor.close();
403 return comp.toByteArray();
404 } else {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800405 return DB_SERIALIZER.encode(state);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800406 }
Madan Jampani08822c42014-11-04 17:17:46 -0800407 } catch (Exception e) {
Madan Jampani778f7ad2014-11-05 22:46:15 -0800408 log.error("Failed to take snapshot", e);
409 throw new SnapshotException(e);
Madan Jampani08822c42014-11-04 17:17:46 -0800410 }
411 }
412
413 @Override
414 public void installSnapshot(byte[] data) {
415 try {
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800416 if (compressSnapshot) {
417 ByteArrayInputStream in = new ByteArrayInputStream(data);
418 InflaterInputStream decompressor = new InflaterInputStream(in);
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800419 this.state = DB_SERIALIZER.decode(decompressor);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800420 } else {
Yuta HIGUCHI91768e32014-11-22 05:06:35 -0800421 this.state = DB_SERIALIZER.decode(data);
Yuta HIGUCHIa7680a32014-11-06 22:17:37 -0800422 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800423
Madan Jampania88d1f52014-11-14 16:45:24 -0800424 updatesExecutor.submit(new Runnable() {
425 @Override
426 public void run() {
427 for (DatabaseUpdateEventListener listener : listeners) {
428 listener.snapshotInstalled(state);
429 }
430 }
431 });
432
Madan Jampani08822c42014-11-04 17:17:46 -0800433 } catch (Exception e) {
Madan Jampani778f7ad2014-11-05 22:46:15 -0800434 log.error("Failed to install from snapshot", e);
435 throw new SnapshotException(e);
Madan Jampani08822c42014-11-04 17:17:46 -0800436 }
437 }
Madan Jampani12390c12014-11-12 00:35:56 -0800438
Madan Jampanidef2c652014-11-12 13:50:10 -0800439 /**
440 * Adds specified DatabaseUpdateEventListener.
441 * @param listener listener to add
442 */
Madan Jampani12390c12014-11-12 00:35:56 -0800443 public void addEventListener(DatabaseUpdateEventListener listener) {
444 listeners.add(listener);
445 }
Madan Jampanidef2c652014-11-12 13:50:10 -0800446
447 /**
448 * Removes specified DatabaseUpdateEventListener.
449 * @param listener listener to remove
450 */
451 public void removeEventListener(DatabaseUpdateEventListener listener) {
452 listeners.remove(listener);
453 }
Madan Jampani08822c42014-11-04 17:17:46 -0800454}