/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.streams.mongo; import org.apache.streams.config.ComponentConfigurator; import org.apache.streams.config.StreamsConfigurator; import org.apache.streams.core.StreamsDatum; import org.apache.streams.core.StreamsPersistWriter; import org.apache.streams.jackson.StreamsJacksonMapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.MongoClient; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; import com.mongodb.util.JSON; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import java.util.stream.Stream; public class MongoPersistWriter implements StreamsPersistWriter, Runnable, Flushable, Closeable { public static final String STREAMS_ID = "MongoPersistWriter"; private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistWriter.class); private static final long MAX_WRITE_LATENCY = 1000; protected volatile Queue<StreamsDatum> persistQueue; private ObjectMapper mapper = StreamsJacksonMapper.getInstance(); private volatile AtomicLong lastWrite = new AtomicLong(System.currentTimeMillis()); private ScheduledExecutorService backgroundFlushTask = Executors.newSingleThreadScheduledExecutor(); private MongoConfiguration config; protected MongoClient client; protected DB db; protected DBCollection collection; protected List<DBObject> insertBatch = new ArrayList<>(); protected final ReadWriteLock lock = new ReentrantReadWriteLock(); public MongoPersistWriter() { this(new ComponentConfigurator<>(MongoConfiguration.class) .detectConfiguration(StreamsConfigurator.getConfig().getConfig("mongo"))); } public MongoPersistWriter(MongoConfiguration config) { this.config = config; } public void setPersistQueue(Queue<StreamsDatum> persistQueue) { this.persistQueue = persistQueue; } public Queue<StreamsDatum> getPersistQueue() { return persistQueue; } @Override public String getId() { return STREAMS_ID; } @Override public void write(StreamsDatum streamsDatum) { DBObject dbObject = prepareObject(streamsDatum); if (dbObject != null) { addToBatch(dbObject); flushIfNecessary(); } } @Override public void flush() throws IOException { try { LOGGER.debug("Attempting to flush {} items to mongo", insertBatch.size()); lock.writeLock().lock(); collection.insert(insertBatch); lastWrite.set(System.currentTimeMillis()); insertBatch = new ArrayList<>(); } finally { lock.writeLock().unlock(); } } public synchronized void close() throws IOException { client.close(); backgroundFlushTask.shutdownNow(); } /** * start write thread. */ public void start() { connectToMongo(); backgroundFlushTask.scheduleAtFixedRate(this::flushIfNecessary, 0, MAX_WRITE_LATENCY * 2, TimeUnit.MILLISECONDS); } /** * stop. */ public void stop() { try { flush(); } catch (IOException ex) { LOGGER.error("Error flushing", ex); } try { close(); } catch (IOException ex) { LOGGER.error("Error closing", ex); } try { backgroundFlushTask.shutdown(); // Wait a while for existing tasks to terminate if (!backgroundFlushTask.awaitTermination(15, TimeUnit.SECONDS)) { backgroundFlushTask.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if (!backgroundFlushTask.awaitTermination(15, TimeUnit.SECONDS)) { LOGGER.error("Stream did not terminate"); } } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted backgroundFlushTask.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } } @Override public void run() { while (true) { if (persistQueue.peek() != null) { try { StreamsDatum entry = persistQueue.remove(); write(entry); } catch (Exception ex) { ex.printStackTrace(); } } try { Thread.sleep(new Random().nextInt(1)); } catch (InterruptedException interrupt) { LOGGER.trace("Interrupt", interrupt); } } } @Override public void prepare(Object configurationObject) { this.persistQueue = new ConcurrentLinkedQueue<>(); start(); } @Override public void cleanUp() { stop(); } protected void flushIfNecessary() { long lastLatency = System.currentTimeMillis() - lastWrite.get(); //Flush iff the size > 0 AND the size is divisible by 100 or the time between now and the last flush is greater //than the maximum desired latency if (insertBatch.size() > 0 && (insertBatch.size() % 100 == 0 || lastLatency > MAX_WRITE_LATENCY)) { try { flush(); } catch (IOException ex) { LOGGER.error("Error writing to Mongo", ex); } } } protected void addToBatch(DBObject dbObject) { try { lock.readLock().lock(); insertBatch.add(dbObject); } finally { lock.readLock().unlock(); } } protected DBObject prepareObject(StreamsDatum streamsDatum) { DBObject dbObject = null; if (streamsDatum.getDocument() instanceof String) { dbObject = (DBObject) JSON.parse((String) streamsDatum.getDocument()); } else { try { ObjectNode node = mapper.valueToTree(streamsDatum.getDocument()); dbObject = (DBObject) JSON.parse(node.toString()); } catch (Exception ex) { LOGGER.error("Unsupported type: " + streamsDatum.getDocument().getClass(), ex); } } return dbObject; } private synchronized void connectToMongo() { ServerAddress serverAddress = new ServerAddress(config.getHost(), config.getPort().intValue()); if (StringUtils.isNotEmpty(config.getUser()) && StringUtils.isNotEmpty(config.getPassword())) { MongoCredential credential = MongoCredential.createCredential(config.getUser(), config.getDb(), config.getPassword().toCharArray()); client = new MongoClient(serverAddress, Stream.of(credential).collect(Collectors.toList())); } else { client = new MongoClient(serverAddress); } db = client.getDB(config.getDb()); if (!db.collectionExists(config.getCollection())) { db.createCollection(config.getCollection(), null); } collection = db.getCollection(config.getCollection()); } }