/* Copyright (c) 2011 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.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.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 dk.dma.ais.tracker.targetTracker; import com.google.common.util.concurrent.AbstractScheduledService; import dk.dma.ais.packet.AisPacketSource; import dk.dma.ais.tracker.targetTracker.TargetTracker.MmsiTarget; import dk.dma.commons.util.io.IoUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import static java.util.Objects.requireNonNull; /** * Takes care of backing up and restoring in case of a crash. * * @author Kasper Nielsen */ public class TargetTrackerFileBackupService extends AbstractScheduledService { /** The logger. */ private static final Logger LOG = LoggerFactory.getLogger(TargetTrackerFileBackupService.class); /** The folder to backup files to. */ private final Path backupFolder; BackupFile f; /** A random string appended to each backup file. To distinguish them from backup files created by former runs. */ private final String prefix = Integer.toString(ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE), 36) .toUpperCase(); /** The tracker that we are make backups and restoring from. */ private final dk.dma.ais.tracker.targetTracker.TargetTracker tracker; /** A list of those targets that have been backed up. */ private final Set<dk.dma.ais.tracker.targetTracker.TargetInfo> backedUpTargets = Collections .newSetFromMap(new IdentityHashMap<dk.dma.ais.tracker.targetTracker.TargetInfo, Boolean>()); /** * Creates a new backup service. * * @param tracker * the target tracker that we are make backups and restoring from * @param backupFolder * the folder to backup and restore from */ public TargetTrackerFileBackupService(dk.dma.ais.tracker.targetTracker.TargetTracker tracker, Path backupFolder) { this.tracker = requireNonNull(tracker); this.backupFolder = requireNonNull(backupFolder); } void restoreBackupFiles() throws IOException, ClassNotFoundException { List<Path> paths = new ArrayList<>(); try (DirectoryStream<Path> stream = Files.newDirectoryStream(backupFolder)) { for (Path path : stream) { if (path.getFileName().toString().endsWith("-00")) { paths.add(path); } } } Path latestFull = IoUtil.findLatestModified(paths); if (latestFull != null) { LOG.info("Trying to restore backup files " + latestFull); String format = latestFull.getFileName().toString(); format = format.substring(0, format.length() - 3);// strip -00 ArrayList<Path> files = new ArrayList<>(); for (Path path : Files.newDirectoryStream(backupFolder)) { if (path.getFileName().toString().startsWith(format)) { files.add(path); } } Collections.sort(files); // make sure they are read in the right order for (Path path : files) { LOG.info("Restoring backup file " + path); try (InputStream fos = Files.newInputStream(path); BufferedInputStream bos = new BufferedInputStream(fos); GZIPInputStream gos = new GZIPInputStream(bos); ObjectInputStream oos = new ObjectInputStream(gos)) { AisPacketSource sb = (AisPacketSource) oos.readObject(); dk.dma.ais.tracker.targetTracker.TargetInfo ti = (dk.dma.ais.tracker.targetTracker.TargetInfo) oos.readObject(); while (sb != null && ti != null) { tracker.update(sb, ti); sb = (AisPacketSource) oos.readObject(); ti = (dk.dma.ais.tracker.targetTracker.TargetInfo) oos.readObject(); } } } } } /** {@inheritDoc} */ @Override protected void startUp() throws Exception{ LOG.info("{} startUp", TargetTrackerFileBackupService.class); // If this is the first run, we check if there are any files to restore try { restoreBackupFiles(); } catch (Exception e) { LOG.error("Cannot restore from backup", e); } f = new BackupFile(); } /** {@inheritDoc} */ @Override protected void runOneIteration() throws Exception { Path p = f.toPath(backupFolder, prefix); try { if (f.isFull()) { backedUpTargets.clear(); // clear all backed up files } writeBackupFile(p); f = f.next(); } catch (Exception e) { LOG.error("Failed to write backup to " + p, e); f = f.nextFull(); } } /** {@inheritDoc} */ @Override protected Scheduler scheduler() { return Scheduler.newFixedRateSchedule(0, 1, TimeUnit.SECONDS); } private void writeBackupFile(Path p) throws IOException { // We write to a temporary file, to make sure we only have complete valid backup files in the folder Path temporaryFile = Files.createTempFile(p.getFileName().toString(), ".tmp"); boolean isFullBackup = true; // Serialize all sourceBundle->TargetInfo mappings try (OutputStream fos = Files.newOutputStream(temporaryFile); BufferedOutputStream bos = new BufferedOutputStream(fos); GZIPOutputStream gos = new GZIPOutputStream(bos); ObjectOutputStream oos = new ObjectOutputStream(gos)) { for (MmsiTarget t : tracker.targets.values()) { for (Entry<AisPacketSource, dk.dma.ais.tracker.targetTracker.TargetInfo> e : t.entrySet()) { // Serialize it, if it is a full backup or if the backup flag has not yet been set if (backedUpTargets.add(e.getValue())) { oos.writeObject(e.getKey()); oos.writeObject(e.getValue()); } else { isFullBackup = false; // at least one file has already been backed up elsewhere } } } // Write footer, 2 nulls mean end of stream oos.writeObject(null); oos.writeObject(null); } // move the temporary file to the correct spot Files.move(temporaryFile, p); // If this backup is a full backup, delete old backup files if (isFullBackup) { try (DirectoryStream<Path> stream = Files.newDirectoryStream(backupFolder)) { for (Path path : stream) { if (!path.equals(p)) { // delete all files but the full backup we just wrote Files.delete(path); } } } } } /** Wrapping the versioning */ static class BackupFile { final int majorBackupVersion; final int minorBackupVersion; BackupFile() { this(1, 0); } private BackupFile(int major, int minor) { this.majorBackupVersion = major; this.minorBackupVersion = minor; } boolean isFull() { return minorBackupVersion == 0; } BackupFile next() { return minorBackupVersion == 99 ? nextFull() : new BackupFile(majorBackupVersion, minorBackupVersion + 1); } BackupFile nextFull() { return new BackupFile((majorBackupVersion + 1) % 100000, 0); } Path toPath(Path directory, String prefix) { return directory.resolve("aisviewer_backup-" + prefix + "-" + new DecimalFormat("00000").format(majorBackupVersion) + "-" + new DecimalFormat("00").format(minorBackupVersion)); } } }