org.ulyssis.ipp.reader.Reader.java Source code

Java tutorial

Introduction

Here is the source code for org.ulyssis.ipp.reader.Reader.java

Source

/*
 * Copyright (C) 2014-2015 ULYSSIS VZW
 *
 * This file is part of i++.
 * 
 * i++ is free software: you can redistribute it and/or modify
 * it under the terms of version 3 of the GNU Affero General Public License
 * as published by the Free Software Foundation. No other versions apply.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 */
package org.ulyssis.ipp.reader;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.llrp.ltk.generated.messages.RO_ACCESS_REPORT;
import org.llrp.ltk.generated.parameters.TagReportData;
import org.llrp.ltk.types.LLRPMessage;
import org.ulyssis.ipp.config.Config;
import org.ulyssis.ipp.config.ReaderConfig;
import org.ulyssis.ipp.control.CommandProcessor;
import org.ulyssis.ipp.control.handlers.PingHandler;
import org.ulyssis.ipp.status.StatusMessage;
import org.ulyssis.ipp.status.StatusReporter;
import org.ulyssis.ipp.updates.TagUpdate;
import org.ulyssis.ipp.utils.JedisHelper;
import org.ulyssis.ipp.utils.Serialization;
import org.ulyssis.ipp.TagId;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;

// TODO: Go over all of the error handling, and evaluate if it is appropriate
// TODO: Set some sort of scheduled task for timeouts, try to reinitialize?

public final class Reader implements Runnable {
    private static final Logger LOG = LogManager.getLogger(Reader.class);

    private final CommandProcessor commandProcessor;
    private final ReaderOptions options;
    private final ReaderConfig readerConfig;
    private final StatusReporter statusReporter;
    private final LLRPReader llrpReader;
    private final ScheduledExecutorService executorService;
    private Optional<ByteChannel> replayChannel = Optional.empty();
    private final String updateChannel;

    // NOTE: be careful, Jedis instances are not threadsafe!
    private final Jedis jedis;

    private long updateCount = 0L;

    private Instant lastUpdate = Instant.now();

    private final Map<TagId, Instant> lastUpdateForTag;
    private boolean speedwayInitialized = false;

    /**
     * Create a new reader and connect to Redis.
     * 
     * options are passed in, rather than
     * accessed through a singleton or such, to improve testability
     * and modularity, and to prevent hidden dependencies and
     * eventual threading issues.
     * 
     * @param options
     *           The command line options to use for this reader.
     */
    public Reader(ReaderOptions options) {
        this.options = options;
        this.readerConfig = Config.getCurrentConfig().getReader(options.getId());
        this.llrpReader = new LLRPReader(this::messageReceived, this::errorOccurred);

        if (readerConfig.getType() == ReaderConfig.Type.SIMULATOR) {
            executorService = Executors.newSingleThreadScheduledExecutor();
        } else {
            executorService = null;
        }

        if (options.getNoRedis()) {
            LOG.info("Not using Redis, setting initial update count to 0.");
            this.updateCount = 0L;
            this.jedis = null;
        } else {
            this.jedis = JedisHelper.get(readerConfig.getURI());
            try {
                this.updateCount = jedis.llen("updates");
            } catch (JedisConnectionException e) {
                LOG.error("Couldn't connect to Jedis when getting update count. Setting 0 instead.", e);
                this.updateCount = 0L; // TODO: Is 0 appropriate?
            }
        }
        String statusChannel = Config.getCurrentConfig().getStatusChannel();
        this.statusReporter = new StatusReporter(readerConfig.getURI(), statusChannel);
        String controlChannel = Config.getCurrentConfig().getControlChannel();
        this.commandProcessor = new CommandProcessor(readerConfig.getURI(), controlChannel, statusReporter);
        commandProcessor.addHandler(new PingHandler());
        this.updateChannel = JedisHelper.dbLocalChannel(Config.getCurrentConfig().getUpdateChannel(),
                readerConfig.getURI());

        options.getReplayFile().ifPresent(replayFile -> {
            try {
                LOG.info("Opening replay file: {}", replayFile);
                ByteChannel channel = Files.newByteChannel(replayFile, StandardOpenOption.APPEND,
                        StandardOpenOption.CREATE);
                this.replayChannel = Optional.of(channel);
            } catch (IOException e) {
                LOG.error("Couldn't open channel for logging to replay file: {}", replayFile, e);
            }
        });

        this.lastUpdateForTag = new HashMap<>();
    }

    /**
     * Run the reader. Reader implements runnable, so that we can
     * do this in its own thread.
     */
    @Override
    public void run() {
        LOG.info("Spinning up reader!");
        ReaderConfig.Type type = Config.getCurrentConfig().getReader(options.getId()).getType();
        if (type == ReaderConfig.Type.LLRP) {
            initSpeedway();
            if (!speedwayInitialized) {
                shutdownHook();
                return;
            }
        } else if (type == ReaderConfig.Type.SIMULATOR) {
            initSimulator();
        }
        Thread commandThread = new Thread(commandProcessor);
        commandThread.start();
        statusReporter.broadcast(new StatusMessage(StatusMessage.MessageType.STARTED_UP,
                String.format("Started up reader %s!", options.getId())));
        try {
            while (!Thread.currentThread().isInterrupted()) {
                Duration maxUpdateInterval = Duration.ofMillis(Config.getCurrentConfig().getMaxUpdateInterval());
                if (maxUpdateInterval.minus(Duration.between(lastUpdate, Instant.now())).isNegative()) {
                    lastUpdate = Instant.now();
                    LOG.warn("No update received in {} seconds!", maxUpdateInterval.getSeconds());
                    statusReporter.broadcast(new StatusMessage(StatusMessage.MessageType.NO_UPDATES,
                            String.format("No update received in %s seconds!", maxUpdateInterval.getSeconds())));
                }
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            // We don't care about this exception
        }
        commandProcessor.stop();
        commandThread.interrupt();
        try {
            commandThread.join();
        } catch (InterruptedException ignored) {
        }
        shutdownHook();
    }

    private void initSpeedway() {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        try {
            while (!speedwayInitialized && !Thread.currentThread().isInterrupted()) {
                // We're doing this in another thread because LLRPReader doesn't
                // interrupt properly.
                Callable<Boolean> runCallable = () -> llrpReader
                        .run(Config.getCurrentConfig().getSpeedwayURI(options.getId()));
                Future<Boolean> initFuture = executor.submit(runCallable);
                try {
                    speedwayInitialized = initFuture.get();
                } catch (ExecutionException e) {
                    LOG.error("Starting the Speedway caused an exception.", e);
                    speedwayInitialized = false;
                }
                if (!speedwayInitialized) {
                    LOG.error("Couldn't start the Speedway! Retrying in {} ms.",
                            Config.getCurrentConfig().getRetryInterval());
                    statusReporter.broadcast(new StatusMessage(StatusMessage.MessageType.STARTUP_FAILURE,
                            "Failed to start the Speedway!"));
                    Thread.sleep(Config.getCurrentConfig().getRetryInterval());
                }
            }
        } catch (InterruptedException e) {
            // Shutting down
        }
    }

    private void initSimulator() {
        for (ReaderConfig.SimulatedTeam team : readerConfig.getSimulatedTeams()) {
            Runnable runnable = () -> simulateOneTeam(team);
            double startingPosition = Config.getCurrentConfig().getTrackLength() / 2.0;
            double distanceToGo;
            if (readerConfig.getPosition() > startingPosition) {
                distanceToGo = readerConfig.getPosition() - startingPosition;
            } else {
                distanceToGo = Config.getCurrentConfig().getTrackLength() - startingPosition
                        + readerConfig.getPosition();
            }
            double avgVelocity = Config.getCurrentConfig().getTrackLength() / team.getLapTime();
            double time = distanceToGo / avgVelocity;
            executorService.schedule(runnable, (long) time, TimeUnit.SECONDS);
        }
    }

    private void simulateOneTeam(ReaderConfig.SimulatedTeam team) {
        Instant instant = Instant.now();
        TagId tag = team.getTag();
        if (acceptUpdate(instant, tag)) {
            pushUpdate(instant, tag);
        }
        Runnable runnable = () -> simulateOneTeam(team);
        executorService.schedule(runnable, team.getLapTime(), TimeUnit.SECONDS);
    }

    /**
     * Perform cleanup on shutdown. (When the thread is interrupted.)
     */
    private void shutdownHook() {
        statusReporter.broadcast(new StatusMessage(StatusMessage.MessageType.SHUTDOWN,
                String.format("Shutting down reader %s.", options.getId())));
        if (speedwayInitialized) {
            LOG.info("Shutting down reader!");
            boolean successfulStop = llrpReader.stop();
            if (!successfulStop) {
                LOG.error("Could not stop the Speedway!");
            } else {
                LOG.info("Successfully stopped the reader!");
            }
        }
        replayChannel.ifPresent(channel -> {
            final Path replayFile = options.getReplayFile().get();
            try {
                channel.close();
            } catch (IOException e) {
                LOG.error("Error while closing replay log file: {}.", replayFile, e);
            }
            try {
                if (Files.size(replayFile) == 0L) {
                    LOG.info("Deleting empty replay file: {}", replayFile);
                    Files.delete(replayFile);
                }
            } catch (IOException e) {
                LOG.error("Couldn't check size of replay log {}, or delete it.", replayFile, e);
            }
        });
        LOG.info("Bye bye!");
    }

    // TODO: Recover in case of lost connection with Speedway.
    public void messageReceived(LLRPMessage msg) {
        if (msg.getTypeNum() == RO_ACCESS_REPORT.TYPENUM) {
            RO_ACCESS_REPORT report = (RO_ACCESS_REPORT) msg;
            List<TagReportData> tags = report.getTagReportDataList();
            for (TagReportData tagReportData : tags) {
                // NOTE: We're using Instant.now() instead of relying on the timestamp
                //          of the update. The delay should be small, and we'll keep the
                //         counting systems in sync using NTP.
                Instant now = Instant.now();
                TagId tag = LLRPReader.decodeEPCParameter(tagReportData.getEPCParameter());
                if (acceptUpdate(now, tag)) {
                    pushUpdate(now, tag);
                }
            }
        }
    }

    public void errorOccurred(String s) {
        LOG.error("An error occurred: {}", s);
        statusReporter.broadcast(new StatusMessage(StatusMessage.MessageType.MISC_ERROR, s));
    }

    /**
     * Check whether this tag code has been seen at least config.getMinUpdateInterval()
     * ago. If not, the update should be ignored.
     */
    private boolean acceptUpdate(Instant now, TagId tag) {
        lastUpdate = now;
        boolean result = !lastUpdateForTag.containsKey(tag)
                || Duration.ofMillis(Config.getCurrentConfig().getMinUpdateInterval())
                        .minus(Duration.between(lastUpdateForTag.get(tag), now)).isNegative();
        if (result) {
            lastUpdateForTag.put(tag, now);
        }
        return result;
    }

    /**
     * Push an update to Redis at the given instant with the given tag.
     *
     * TODO: Refactor: split off a "pusher", so we can use it in the integration tests.
     */
    private void pushUpdate(Instant now, TagId tag) {
        TagUpdate update = new TagUpdate(options.getId(), updateCount, now, tag);
        try {
            byte[] updateBytes = Serialization.getJsonMapper().writeValueAsBytes(update);
            logUpdate(updateBytes);
            if (options.getNoRedis()) {
                updateCount++;
            } else {
                LOG.info("Pushing update {}:{} to Redis, tag: {}", update.getReaderId(), update.getUpdateCount(),
                        tag);
                try {
                    Transaction t = jedis.multi();
                    Response<Long> nextUpdateCount = t.rpush("updates".getBytes(), updateBytes);
                    t.publish(updateChannel, String.valueOf(updateCount));
                    t.exec();
                    updateCount = nextUpdateCount.get();
                } catch (JedisConnectionException e) {
                    LOG.error("Error pushing update {} to Redis.", update.getUpdateCount(), e);
                }
            }
        } catch (JsonProcessingException e) {
            LOG.error("Error formatting update as JSON", e);
        }
    }

    /**
     *  Log an update to the replay log, if enabled.
     */
    private void logUpdate(byte[] updateBytes) {
        LOG.debug("Update: {}", LOG.isDebugEnabled() ? new String(updateBytes) : null);
        replayChannel.ifPresent(channel -> {
            try {
                channel.write(ByteBuffer.wrap(updateBytes));
                byte[] newLine = "\n".getBytes();
                channel.write(ByteBuffer.wrap(newLine));
            } catch (IOException e) {
                LOG.error("Couldn't log to replay file: {}", options.getReplayFile().get(), e);
            }
        });
    }

}