/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.gnu.org/licenses/agpl.txt * * 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 se.streamsource.streamflow.web.infrastructure.event; import java.io.File; import java.io.IOException; import java.util.Date; import java.util.Properties; import jdbm.RecordManager; import jdbm.RecordManagerFactory; import jdbm.RecordManagerOptions; import jdbm.btree.BTree; import jdbm.helper.ByteArraySerializer; import jdbm.helper.LongComparator; import jdbm.helper.LongSerializer; import jdbm.helper.MRU; import jdbm.helper.Serializer; import jdbm.helper.Tuple; import jdbm.helper.TupleBrowser; import jdbm.recman.CacheRecordManager; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.io.Input; import org.qi4j.api.io.Output; import org.qi4j.api.io.Receiver; import org.qi4j.api.io.Sender; import org.qi4j.api.io.Transforms; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.service.Activatable; import org.qi4j.api.service.ServiceComposite; import se.streamsource.streamflow.infrastructure.configuration.FileConfiguration; import se.streamsource.streamflow.infrastructure.event.application.TransactionApplicationEvents; import se.streamsource.streamflow.infrastructure.event.application.source.AbstractApplicationEventStoreMixin; import se.streamsource.streamflow.infrastructure.event.application.source.ApplicationEventSource; import se.streamsource.streamflow.infrastructure.event.application.source.ApplicationEventStore; import se.streamsource.streamflow.infrastructure.event.application.source.ApplicationEventStream; /** * JAVADOC */ @Mixins(JdbmApplicationEventStoreService.JdbmEventStoreMixin.class) public interface JdbmApplicationEventStoreService extends ApplicationEventSource, ApplicationEventStore, ApplicationEventStream, EventManagement, Activatable, ServiceComposite { class JdbmEventStoreMixin extends AbstractApplicationEventStoreMixin implements EventManagement, ApplicationEventSource { @Service FileConfiguration fileConfig; private RecordManager recordManager; private BTree index; private Serializer serializer; private File dataFile; private File databaseFile; private File logFile; public void activate() throws IOException { super.activate(); dataFile = new File(fileConfig.dataDirectory(), identity.identity() + "/applicationevents"); databaseFile = new File(fileConfig.dataDirectory(), identity.identity() + "/events.db"); logFile = new File(fileConfig.dataDirectory(), identity.identity() + "/events.lg"); File directory = dataFile.getAbsoluteFile().getParentFile(); directory.mkdirs(); String name = dataFile.getAbsolutePath(); Properties properties = new Properties(); properties.put(RecordManagerOptions.AUTO_COMMIT, "false"); properties.put(RecordManagerOptions.DISABLE_TRANSACTIONS, "false"); initialize(name, properties); } public void passivate() throws Exception { super.passivate(); logger.info("Close event db"); recordManager.close(); } public void removeAll() throws Exception { // Delete event files passivate(); if (!databaseFile.delete()) throw new IOException("Could not delete event database"); if (!logFile.delete()) throw new IOException("Could not delete event log"); activate(); } public void removeTo(Date date) throws IOException { lock(); try { final TupleBrowser browser = index.browse(); Tuple tuple = new Tuple(); while (browser.getNext(tuple)) { Long key = (Long) tuple.getKey(); if (key <= date.getTime()) { index.remove(key); } else { break; // We're done } } commit(); } catch (IOException ex) { rollback(); } finally { lock.unlock(); } } public Output<String, IOException> restore() { return Transforms.lock(JdbmEventStoreMixin.this.lock, new Output<String, IOException>() { public <SenderThrowableType extends Throwable> void receiveFrom(Sender<? extends String, SenderThrowableType> sender) throws IOException, SenderThrowableType { try { sender.sendTo(new Receiver<String, IOException>() { int count = 0; public void receive(String item) throws IOException { try { JSONObject json = (JSONObject) new JSONTokener(item).nextValue(); TransactionApplicationEvents transactionDomain = (TransactionApplicationEvents) transactionEventsType.fromJSON(json, module); storeEvents(transactionDomain); count++; if (count % 1000 == 0) commit(); // Commit every 1000 transactions to avoid OutOfMemory issues } catch (JSONException e) { throw new IOException(e); } } }); commit(); } catch (IOException e) { rollback(); throw e; } catch (Throwable senderThrowableType) { rollback(); throw (SenderThrowableType) senderThrowableType; } } }); } // EventStore implementation public Input<TransactionApplicationEvents, IOException> transactionsAfter(final long afterTimestamp, final long maxTransactions) { return new Input<TransactionApplicationEvents, IOException>() { public <ReceiverThrowableType extends Throwable> void transferTo(Output<? super TransactionApplicationEvents, ReceiverThrowableType> output) throws IOException, ReceiverThrowableType { // Lock datastore first lock(); final Long afterTime = afterTimestamp + 1; try { final TupleBrowser browser = index.browse(afterTime); output.receiveFrom(new Sender<TransactionApplicationEvents, IOException>() { public <ReceiverThrowableType extends Throwable> void sendTo(Receiver<? super TransactionApplicationEvents, ReceiverThrowableType> receiver) throws ReceiverThrowableType, IOException { Tuple tuple = new Tuple(); long count = 0; while (browser.getNext(tuple)) { // Get next transaction TransactionApplicationEvents applicationEvents = readTransactionEvents(tuple); receiver.receive(applicationEvents); count++; if (count == maxTransactions) break; } } }); } catch (Exception e) { logger.warn("Could not iterate transactions", e); } finally { lock.unlock(); } } }; } public Input<TransactionApplicationEvents, IOException> transactionsBefore(final long beforeTimestamp, final long maxTransactions) { return new Input<TransactionApplicationEvents, IOException>() { public <ReceiverThrowableType extends Throwable> void transferTo(Output<? super TransactionApplicationEvents, ReceiverThrowableType> output) throws IOException, ReceiverThrowableType { // Lock datastore first lock(); final Long beforeTime = beforeTimestamp - 1; try { final TupleBrowser browser = index.browse(beforeTime); output.receiveFrom(new Sender<TransactionApplicationEvents, IOException>() { public <ReceiverThrowableType extends Throwable> void sendTo(Receiver<? super TransactionApplicationEvents, ReceiverThrowableType> receiver) throws ReceiverThrowableType, IOException { Tuple tuple = new Tuple(); long count = 0; while (browser.getPrevious(tuple)) { // Get previous transaction TransactionApplicationEvents applicationEvents = readTransactionEvents(tuple); receiver.receive(applicationEvents); count++; if (count == maxTransactions) break; } } }); } catch (Exception e) { logger.warn("Could not iterate transactions", e); } finally { lock.unlock(); } } }; } protected void rollback() throws IOException { recordManager.rollback(); } protected void commit() throws IOException { recordManager.commit(); } protected void storeEvents(TransactionApplicationEvents transactionApplication) throws IOException { try { String jsonString = transactionApplication.toJSON(); index.insert(transactionApplication.timestamp().get(), jsonString.getBytes("UTF-8"), false); recordManager.commit(); } catch (IOException e) { recordManager.rollback(); throw e; } } private void initialize(String name, Properties properties) throws IOException { recordManager = RecordManagerFactory.createRecordManager(name, properties); serializer = new ByteArraySerializer(); recordManager = new CacheRecordManager(recordManager, new MRU(1000)); long recid = recordManager.getNamedObject("index"); if (recid != 0) { index = BTree.load(recordManager, recid); } else { LongComparator comparator = new LongComparator(); index = BTree.createInstance(recordManager, comparator, new LongSerializer(), serializer, 16); recordManager.setNamedObject("index", index.getRecid()); } commit(); } private TransactionApplicationEvents readTransactionEvents(Tuple tuple) throws IOException { try { byte[] eventData = (byte[]) tuple.getValue(); String eventJson = new String(eventData, "UTF-8"); JSONTokener tokener = new JSONTokener(eventJson); JSONObject transaction = (JSONObject) tokener.nextValue(); return (TransactionApplicationEvents) transactionEventsType.fromJSON(transaction, module); } catch (JSONException e) { throw new IOException(e); } } } }