All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jumpmind.symmetric.service.impl.DataLoaderService Maven / Gradle / Ivy

Go to download

SymmetricDS is an open source database synchronization solution. It is platform-independent, web-enabled, and database-agnostic. SymmetricDS was first built to replicate changes between 'retail store' databases and ad centralized 'corporate' database.

The newest version!
/*
 * Licensed to JumpMind Inc under one or more contributor 
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding 
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU Lesser General Public License (the
 * "License"); you may not use this file except in compliance
 * with the License. 
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see           
 * .
 * 
 * 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.jumpmind.symmetric.service.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.jumpmind.symmetric.common.Constants;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.db.IDbDialect;
import org.jumpmind.symmetric.io.ThresholdFileWriter;
import org.jumpmind.symmetric.load.IBatchListener;
import org.jumpmind.symmetric.load.IColumnFilter;
import org.jumpmind.symmetric.load.IDataLoader;
import org.jumpmind.symmetric.load.IDataLoaderContext;
import org.jumpmind.symmetric.load.IDataLoaderFilter;
import org.jumpmind.symmetric.load.IDataLoaderStatistics;
import org.jumpmind.symmetric.model.ChannelMap;
import org.jumpmind.symmetric.model.IncomingBatch;
import org.jumpmind.symmetric.model.Node;
import org.jumpmind.symmetric.model.NodeSecurity;
import org.jumpmind.symmetric.service.IConfigurationService;
import org.jumpmind.symmetric.service.IDataLoaderService;
import org.jumpmind.symmetric.service.IIncomingBatchService;
import org.jumpmind.symmetric.service.INodeService;
import org.jumpmind.symmetric.service.RegistrationNotOpenException;
import org.jumpmind.symmetric.service.RegistrationRequiredException;
import org.jumpmind.symmetric.statistic.IStatisticManager;
import org.jumpmind.symmetric.transport.AuthenticationException;
import org.jumpmind.symmetric.transport.ConnectionRejectedException;
import org.jumpmind.symmetric.transport.IIncomingTransport;
import org.jumpmind.symmetric.transport.ITransportManager;
import org.jumpmind.symmetric.transport.SyncDisabledException;
import org.jumpmind.symmetric.transport.TransportException;
import org.jumpmind.symmetric.transport.file.FileIncomingTransport;
import org.jumpmind.symmetric.transport.internal.InternalIncomingTransport;
import org.jumpmind.symmetric.util.AppUtils;
import org.jumpmind.symmetric.web.WebConstants;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;

/**
 * 
 */
public class DataLoaderService extends AbstractService implements IDataLoaderService, BeanFactoryAware {

    private IDbDialect dbDialect;

    private IIncomingBatchService incomingBatchService;
    
    private IConfigurationService configurationService;

    private ITransportManager transportManager;

    private BeanFactory beanFactory;

    private List filters;

    private IStatisticManager statisticManager;

    private INodeService nodeService;

    private Map> columnFilters = new HashMap>();

    private List batchListeners;

    /**
     * Connect to the remote node and pull data. The acknowledgment of commit/error status is sent separately after the
     * data is processed.
     */
    public boolean loadData(Node remote, Node local) throws IOException {
        boolean wasWorkDone = false;
        try {
            NodeSecurity localSecurity = nodeService.findNodeSecurity(local.getNodeId());
            if (localSecurity != null) {
                Map requestProperties = new HashMap();
                ChannelMap suspendIgnoreChannels = configurationService.getSuspendIgnoreChannelLists();
                requestProperties.put(WebConstants.SUSPENDED_CHANNELS, suspendIgnoreChannels.getSuspendChannelsAsString());
                requestProperties.put(WebConstants.IGNORED_CHANNELS, suspendIgnoreChannels.getIgnoreChannelsAsString());

                List list = loadDataAndReturnBatches(transportManager.getPullTransport(remote, local, localSecurity.getNodePassword(), requestProperties, parameterService.getRegistrationUrl()));
                if (list.size() > 0) {
                    sendAck(remote, local, localSecurity, list);
                    wasWorkDone = true;
                }
            } else {
                log.error("NodeSecurityMissing", local.getNodeId());
            }
        } catch (RegistrationRequiredException e) {
            log.warn("RegistrationLost");
            loadData(transportManager.getRegisterTransport(local, parameterService.getRegistrationUrl()));
            nodeService.findIdentity(false);
            wasWorkDone = true;
        } catch (MalformedURLException e) {
            log.error("URLConnectingFailure", remote.getNodeId(), remote.getSyncUrl());
        }
        return wasWorkDone;
    }

    /**
     * Try a configured number of times to get the ACK through.
     */
    private void sendAck(Node remote, Node local, NodeSecurity localSecurity, List list) throws IOException {
        Exception error = null;
        int sendAck = -1;
        int numberOfStatusSendRetries = parameterService.getInt(ParameterConstants.DATA_LOADER_NUM_OF_ACK_RETRIES);
        for (int i = 0; i < numberOfStatusSendRetries && sendAck != HttpURLConnection.HTTP_OK; i++) {
            try {
                sendAck = transportManager.sendAcknowledgement(remote, list, local, localSecurity.getNodePassword(), parameterService.getRegistrationUrl());
            } catch (IOException ex) {
                log.warn("AckSendingFailed", (i + 1), ex.getMessage());
                error = ex;
            } catch (RuntimeException ex) {
                log.warn("AckSendingFailed", (i + 1), ex.getMessage());
                error = ex;
            }
            if (sendAck != HttpURLConnection.HTTP_OK) {
                if (i < numberOfStatusSendRetries - 1) {
                    AppUtils.sleep(parameterService.getLong(ParameterConstants.DATA_LOADER_TIME_BETWEEN_ACK_RETRIES));
                } else if (error instanceof RuntimeException) {
                    throw (RuntimeException) error;
                } else if (error instanceof IOException) {
                    throw (IOException) error;
                } else {
                    throw new IOException(Integer.toString(sendAck));
                }
            }
        }
    }

    public IDataLoader openDataLoader(BufferedReader reader) throws IOException {
        IDataLoader dataLoader = (IDataLoader) beanFactory.getBean(Constants.DATALOADER);
        dataLoader.open(reader, filters, columnFilters);
        return dataLoader;
    }

    public IDataLoaderStatistics loadDataBatch(String batchData) throws IOException {
        BufferedReader reader = new BufferedReader(new StringReader(batchData));
        IDataLoader dataLoader = openDataLoader(reader);
        IDataLoaderStatistics stats = null;
        try {
            while (dataLoader.hasNext()) {
                dataLoader.load();
                IncomingBatch history = new IncomingBatch(dataLoader.getContext());
                history.setValues(dataLoader.getStatistics(), true);
                fireBatchComplete(dataLoader, history);
            }
        } finally {
            stats = dataLoader.getStatistics();
            dataLoader.close();
        }
        return stats;
    }

    /**
     * Load database from input stream and return a list of batch statuses. This is used for a pull request that
     * responds with data, and the acknowledgment is sent later.
     */
    protected List loadDataAndReturnBatches(IIncomingTransport transport) throws IOException {

        List list = new ArrayList();
        IncomingBatch batch = null;
        IDataLoader dataLoader = null;
        try {
            long totalNetworkMillis = System.currentTimeMillis();
            if (parameterService.is(ParameterConstants.STREAM_TO_FILE_ENABLED)) {
                transport = writeToFile(transport);
                totalNetworkMillis = System.currentTimeMillis() - totalNetworkMillis;
            }
            dataLoader = openDataLoader(transport.open());
            IDataLoaderContext context = dataLoader.getContext();
            while (dataLoader.hasNext()) {
                batch = new IncomingBatch(context);
                if (parameterService.is(ParameterConstants.DATA_LOADER_ENABLED) || 
                    (batch.getChannelId() != null && batch.getChannelId().equals(Constants.CHANNEL_CONFIG))) {
                    list.add(batch);
                    loadBatch(dataLoader, batch);
                }
                batch = null;
            }

            if (parameterService.is(ParameterConstants.STREAM_TO_FILE_ENABLED)) {
                estimateNetworkMillis(list, totalNetworkMillis);
            }

            for (IncomingBatch incomingBatch : list) {
                if (incomingBatch.isPersistable()) {
                    incomingBatchService.updateIncomingBatch(incomingBatch);
                }
            }

        } catch (RegistrationRequiredException ex) {
            throw ex;
        } catch (ConnectException ex) {
            throw ex;
        } catch (UnknownHostException ex) {
            log.warn("TransportFailedUnknownHost", ex.getMessage());
            throw ex;
        } catch (RegistrationNotOpenException ex) {
            log.warn("RegistrationFailed");
        } catch (ConnectionRejectedException ex) {
            log.warn("TransportFailedConnectionBusy");
            throw ex;
        } catch (AuthenticationException ex) {
            log.warn("AuthenticationFailed");
        } catch (SyncDisabledException ex) {
            log.warn("SyncDisabled");
            throw ex;
        } catch (Throwable e) {
            if (dataLoader != null && dataLoader.getContext().getBatchId() > 0 && batch == null) {
                batch = new IncomingBatch(dataLoader.getContext());
                list.add(batch);
            }
            if (dataLoader != null && batch != null) {
                statisticManager.incrementDataLoadedErrors(batch.getChannelId(), 1);
                if (e instanceof IOException || e instanceof TransportException) {
                    log.warn("BatchLoadingFailed", batch.getNodeBatchId(), e.getMessage());
                    batch.setSqlMessage(e.getMessage());
                } else {
                    log.error("BatchLoadingFailed", e, batch.getNodeBatchId(), e.getMessage());
                    SQLException se = unwrapSqlException(e);
                    if (se != null) {                        
                        batch.setSqlState(se.getSQLState());
                        batch.setSqlCode(se.getErrorCode());
                        batch.setSqlMessage(se.getMessage());
                    } else {
                        batch.setSqlMessage(e.getMessage());
                    }
                }
                batch.setValues(dataLoader.getStatistics(), false);
                handleBatchError(batch);
            } else {
                if (e instanceof IOException) {
                    if (!e.getMessage().startsWith("http")) {
                        log.error("BatchReadingFailed", e.getMessage());
                    } else {
                        log.error("BatchReadingFailed", e.getMessage(), e);
                    }
                } else {
                    log.error("BatchParsingFailed", e);
                }
            }
        } finally {
            if (dataLoader != null) {
                dataLoader.close();
            }
            transport.close();
        }
        return list;
    }

    protected void estimateNetworkMillis(List list, long totalNetworkMillis) {
        long totalNumberOfBytes = 0;
        for (IncomingBatch incomingBatch : list) {
            totalNumberOfBytes += incomingBatch.getByteCount();
        }
        for (IncomingBatch incomingBatch : list) {
            if (totalNumberOfBytes > 0) {
                double ratio = (double) incomingBatch.getByteCount() / (double) totalNumberOfBytes;
                incomingBatch.setNetworkMillis((long) (totalNetworkMillis * ratio));
            }
        }
    }

    protected IIncomingTransport writeToFile(IIncomingTransport transport) throws IOException {
        ThresholdFileWriter writer = null;
        try {
            writer = new ThresholdFileWriter(parameterService.getLong(ParameterConstants.STREAM_TO_FILE_THRESHOLD),
                    "load");
            IOUtils.copy(transport.open(), writer);
        } finally {
            IOUtils.closeQuietly(writer);
            transport.close();
        }
        return new FileIncomingTransport(writer);
    }

    public boolean loadData(IIncomingTransport transport) throws IOException {
        boolean inError = false;
        List list = loadDataAndReturnBatches(transport);
        if (list != null && list.size() > 0) {
            for (IncomingBatch incomingBatch : list) {
                inError |= incomingBatch.getStatus() != org.jumpmind.symmetric.model.IncomingBatch.Status.OK;
            }
        } else {
            inError = true;
        }
        return !inError;
    }

    private void fireEarlyCommit(IDataLoader loader, IncomingBatch batch) {
        if (batchListeners != null) {
            long ts = System.currentTimeMillis();
            for (IBatchListener listener : batchListeners) {
                listener.earlyCommit(loader, batch);
            }
            // update the filter milliseconds so batch listeners are also
            // included
            batch.setFilterMillis(batch.getFilterMillis() + (System.currentTimeMillis() - ts));
        }
    }

    private void fireBatchComplete(IDataLoader loader, IncomingBatch batch) {
        if (batchListeners != null) {
            long ts = System.currentTimeMillis();
            for (IBatchListener listener : batchListeners) {
                listener.batchComplete(loader, batch);
            }
            // update the filter milliseconds so batch listeners are also
            // included
            batch.setFilterMillis(batch.getFilterMillis() + (System.currentTimeMillis() - ts));
        }
    }

    private void fireBatchCommitted(IDataLoader loader, IncomingBatch batch) {
        if (batchListeners != null) {
            long ts = System.currentTimeMillis();
            for (IBatchListener listener : batchListeners) {
                listener.batchCommitted(loader, batch);
            }
            // update the filter milliseconds so batch listeners are also
            // included
            batch.setFilterMillis(batch.getFilterMillis() + (System.currentTimeMillis() - ts));
        }
    }

    private void fireBatchRolledback(IDataLoader loader, IncomingBatch batch, Exception ex) {
        if (batchListeners != null) {
            long ts = System.currentTimeMillis();
            for (IBatchListener listener : batchListeners) {
                listener.batchRolledback(loader, batch, ex);
            }
            // update the filter milliseconds so batch listeners are also
            // included
            batch.setFilterMillis(batch.getFilterMillis() + (System.currentTimeMillis() - ts));
        }
    }

    protected void handleBatchError(final IncomingBatch status) {
        try {
            if (!status.isRetry()) {
                status.setStatus(IncomingBatch.Status.ER);
                incomingBatchService.insertIncomingBatch(status);
            }
        } catch (Exception e) {
            log.error("BatchStatusRecordFailed", status.getNodeBatchId());
        }
    }

    /**
     * Load database from input stream and write acknowledgment to output stream. This is used for a "push" request with
     * a response of an acknowledgment.
     */
    public void loadData(InputStream in, OutputStream out) throws IOException {
        List list = loadDataAndReturnBatches(new InternalIncomingTransport(in));
        Node local = nodeService.findIdentity();
        NodeSecurity security = nodeService.findNodeSecurity(local.getNodeId());
        transportManager.writeAcknowledgement(out, list, local, security != null ? security.getNodePassword() : null);
    }

    public void setDataLoaderFilters(List filters) {
        this.filters = filters;
    }

    public void addDataLoaderFilter(IDataLoaderFilter filter) {
        if (filters == null) {
            filters = new ArrayList();
        }
        filters.add(filter);
    }

    public void removeDataLoaderFilter(IDataLoaderFilter filter) {
        filters.remove(filter);
    }

    public void setTransportManager(ITransportManager remoteService) {
        this.transportManager = remoteService;
    }

    public void setIncomingBatchService(IIncomingBatchService incomingBatchService) {
        this.incomingBatchService = incomingBatchService;
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void setDbDialect(IDbDialect dbDialect) {
        this.dbDialect = dbDialect;
    }

    public void addColumnFilter(String tableName, IColumnFilter filter) {
        List filters = this.columnFilters.get(tableName);
        if (filters == null) {
            filters = new ArrayList();
            this.columnFilters.put(tableName, filters);
        }
        filters.add(filter);
    }
    
    /**
     * @see IDataLoaderService#reRegisterColumnFilter(String[], IColumnFilter)
     */
    public void reRegisterColumnFilter(String[] tableNames, IColumnFilter filter) {
        Set>> entries = this.columnFilters.entrySet();
        Iterator>> it = entries.iterator();
        while (it.hasNext()) {
            Entry> entry = it.next();
            if (entry.getValue().contains(filter)) {
                entry.getValue().remove(filter);
            }            
        }
        
        if (tableNames != null) {
            for (String name : tableNames) {
                this.addColumnFilter(name, filter);
            }
        }        
    }

    public void setStatisticManager(IStatisticManager statisticManager) {
        this.statisticManager = statisticManager;
    }

    public void addBatchListener(IBatchListener batchListener) {
        if (this.batchListeners == null) {
            this.batchListeners = new ArrayList();
        }
        this.batchListeners.add(batchListener);
    }

    public void setBatchListeners(List batchListeners) {
        this.batchListeners = batchListeners;
    }

    public void setNodeService(INodeService nodeService) {
        this.nodeService = nodeService;
    }
    
    public void setConfigurationService(IConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    private enum LoadStatus {
        CONTINUE, DONE
    }

    protected void loadBatch(final IDataLoader dataLoader, final IncomingBatch batch) {
        try {
            TransactionalLoadDelegate loadDelegate = new TransactionalLoadDelegate(batch, dataLoader);
            LoadStatus loadStatus = loadDelegate.getLoadStatus();
            do {
                newTransactionTemplate.execute(loadDelegate);
                loadStatus = loadDelegate.getLoadStatus();
                if (loadStatus == LoadStatus.CONTINUE) {
                    // Chances are if SymmetricDS is configured to commit early in a batch we want to give other threads
                    // a chance to do work and access the database.
                    AppUtils.sleep(5);
                }
            } while (LoadStatus.CONTINUE == loadStatus);
            fireBatchCommitted(dataLoader, batch);
        } catch (RuntimeException ex) {
            fireBatchRolledback(dataLoader, batch, ex);
            throw ex;
        }
    }

    class TransactionalLoadDelegate implements TransactionCallback {
        IncomingBatch batch;
        IDataLoader dataLoader;
        LoadStatus loadStatus = LoadStatus.DONE;

        public TransactionalLoadDelegate(IncomingBatch status, IDataLoader dataLoader) {
            this.batch = status;
            this.dataLoader = dataLoader;
        }

        public LoadStatus doInTransaction(TransactionStatus txStatus) {
            try {
                boolean done = true;
                dbDialect.disableSyncTriggers(dataLoader.getContext().getSourceNodeId());
                if (this.loadStatus == LoadStatus.CONTINUE || incomingBatchService.acquireIncomingBatch(batch)) {
                    done = dataLoader.load();
                } else {
                    dataLoader.skip();
                }
                batch.setValues(dataLoader.getStatistics(), true);
                if (done) {
                    fireBatchComplete(dataLoader, batch);
                    this.loadStatus = LoadStatus.DONE;
                } else {
                    log.info("LoaderEarlyCommit", batch.getBatchId(), dataLoader.getContext().getTableName(), dataLoader.getStatistics().getLineCount());
                    fireEarlyCommit(dataLoader, batch);
                    this.loadStatus = LoadStatus.CONTINUE;
                }
                return this.loadStatus;
            } catch (IOException e) {
                throw new TransportException(e);
            } finally {
                dbDialect.enableSyncTriggers();
            }
        }

        public LoadStatus getLoadStatus() {
            return loadStatus;
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy