package com.after_sunrise.oss.otdb.je.loader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
import com.after_sunrise.commons.log.object.Logs;
import com.after_sunrise.oss.otdb.api.loader.LoadableTick;
import com.after_sunrise.oss.otdb.api.loader.LoadableTickIterator;
import com.after_sunrise.oss.otdb.api.loader.LoadableTickLoader;
import com.after_sunrise.oss.otdb.je.database.CodeDatabase;
import com.after_sunrise.oss.otdb.je.database.ForwardIterator;
import com.after_sunrise.oss.otdb.je.database.SourceDatabase;
import com.after_sunrise.oss.otdb.je.database.TickDatabase;
import com.after_sunrise.oss.otdb.je.entity.TickEntry;
import com.after_sunrise.oss.otdb.je.entity.TickKey;
import com.after_sunrise.oss.otdb.je.entity.TickSource;
import com.after_sunrise.oss.otdb.je.entity.TickValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.sleepycat.je.Environment;
import com.sleepycat.je.Transaction;
/**
* @author takanori.takase
*/
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class JeTickLoaderDelegate implements LoadableTickLoader {
private static final String TEMP = "open-tickdb-context-temp.xml";
private final Log log = LogFactory.getLog(getClass());
@Autowired
private Environment environment;
@Autowired
private SourceDatabase sourceDatabase;
@Autowired
private CodeDatabase codeDatabase;
@Autowired
private TickDatabase tickDatabase;
// TODO Handle transaction if needed.
private Transaction tx;
@VisibleForTesting
SourceDatabase getSourceDatabase() {
return sourceDatabase;
}
@VisibleForTesting
CodeDatabase getCodeDatabase() {
return codeDatabase;
}
@VisibleForTesting
TickDatabase getTickDatabase() {
return tickDatabase;
}
@VisibleForTesting
void setTx(Transaction tx) {
this.tx = tx;
}
@Override
public void initialize() throws IOException {
log.info("Initializing service.");
sourceDatabase.initialize(environment, tx);
codeDatabase.initialize(environment, tx);
tickDatabase.initialize(environment, tx);
log.info("Initialized service.");
}
@Override
public void close() throws IOException {
log.info("Closing service.");
IOUtils.closeQuietly(tickDatabase);
IOUtils.closeQuietly(codeDatabase);
IOUtils.closeQuietly(sourceDatabase);
environment.close();
log.info("Closed service.");
}
@Override
public List<String> list() throws IOException {
ForwardIterator<String> itr = sourceDatabase.keyIterator(tx);
ArrayList<String> list = new ArrayList<>();
try {
String key = itr.next();
while (key != null) {
list.add(key);
key = itr.next();
}
} finally {
IOUtils.closeQuietly(itr);
}
list.trimToSize();
return list;
}
@Override
public Long find(String source) throws IOException {
TickSource tickSource = sourceDatabase.search(tx, source);
if (tickSource == null) {
return null;
}
return MoreObjects.firstNonNull(tickSource.getCount(), Long.valueOf(0L));
}
@Override
public Long delete(String source) throws IOException {
TickSource tickSource = sourceDatabase.search(tx, source);
if (tickSource == null) {
return null;
}
long count = 0;
long id = tickSource.getId();
try (ForwardIterator<TickEntry> itr = tickDatabase.entryIterator(tx)) {
for (TickEntry tick = itr.next(); tick != null; tick = itr.next()) {
if (id != tick.getValue().getSourceId()) {
continue;
}
tickDatabase.delete(tx, tick.getKey());
count++;
}
if (count > 0L) {
tickDatabase.sync();
}
sourceDatabase.delete(tx, source);
sourceDatabase.sync();
int cleaned;
while ((cleaned = environment.cleanLog()) > 0) {
Logs.logInfo(log, "Cleaning logs... %s", cleaned);
}
environment.compress();
} catch (IOException e) {
String msg = "Failed to delete ticks. source[%s] id[%s] deleted[%,3d]";
log.error(String.format(msg, source, id, count), e);
throw e;
}
return count;
}
@Override
public long load(String source, LoadableTickIterator iterator)
throws IOException {
if (iterator == null) {
throw new IOException("Iterator cannot be null.");
}
if (StringUtils.isBlank(source)) {
throw new IOException("Source cannot be null.");
}
try {
iterator.initialize();
TickSource tickSource = sourceDatabase.insert(tx, source);
sourceDatabase.sync();
persist(tickSource, iterator);
sourceDatabase.persist(tx, source, tickSource);
sourceDatabase.sync();
return tickSource.getCount();
} finally {
IOUtils.closeQuietly(iterator);
}
}
@VisibleForTesting
void persist(TickSource tickSource, final LoadableTickIterator itr)
throws IOException {
long count = 0L;
ApplicationContext context = new ClassPathXmlApplicationContext(TEMP);
try (JeTickLoaderDelegate temp = context.getBean(getClass())) {
TickDatabase tempDb = retrieveTempDatabase(temp);
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
while (itr.hasNext()) {
LoadableTick tick = itr.getNext();
TickEntry entry = convert(tickSource.getId(), count++, tick);
tempDb.persist(tx, entry);
min = Math.min(min, entry.getKey().getTimestamp());
max = Math.max(max, entry.getKey().getTimestamp());
}
tickSource.setStartTime(min);
tickSource.setEndTime(max);
tickSource.setCount(count);
try (ForwardIterator<TickEntry> iterator = tempDb.entryIterator(tx)) {
persist(count, iterator);
}
} finally {
((ConfigurableApplicationContext) context).close();
}
}
@VisibleForTesting
TickDatabase retrieveTempDatabase(JeTickLoaderDelegate temp)
throws IOException {
Environment env = temp.environment;
for (String dbName : env.getDatabaseNames()) {
// Clean garbage from previous import.
env.truncateDatabase(tx, dbName, false);
}
temp.initialize();
return temp.tickDatabase;
}
@VisibleForTesting
TickEntry convert(long id, long sequence, LoadableTick tick)
throws IOException {
if (tick == null) {
throw new IOException("Tick cannot be null.");
}
String code = tick.getCode();
if (code == null) {
throw new IOException("Code cannot be null.");
}
Long codeId = codeDatabase.search(tx, code);
if (codeId == null) {
codeId = codeDatabase.persist(tx, code);
}
TickKey k = new TickKey(codeId, tick.getTimestamp(), sequence);
TickValue v = new TickValue(id, tick.getDecimals(), tick.getStrings());
return new TickEntry(k, v);
}
@VisibleForTesting
void persist(long count, ForwardIterator<TickEntry> itr) throws IOException {
// Persisted ticks
long persisted = 0L;
// Current sequence id
long currentId = 0L;
// Allocated sequence id
int allocated = 0;
for (TickEntry tick = itr.next(); tick != null; tick = itr.next()) {
if (allocated == 0L) {
long remaining = count - persisted;
allocated = (int) Math.min(remaining, Integer.MAX_VALUE);
currentId = tickDatabase.generateSequenceId(tx, allocated);
}
long codeId = tick.getKey().getCodeId();
long timestamp = tick.getKey().getTimestamp();
TickKey newKey = new TickKey(codeId, timestamp, currentId);
TickEntry entry = new TickEntry(newKey, tick.getValue());
tickDatabase.persist(tx, entry);
persisted++;
currentId++;
allocated--;
}
if (persisted > 0L) {
tickDatabase.sync();
}
}
}