com.spotify.helios.servicescommon.PersistentAtomicReference.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.helios.servicescommon.PersistentAtomicReference.java

Source

/*
 * Copyright (c) 2014 Spotify AB.
 *
 * 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 com.spotify.helios.servicescommon;

import com.spotify.helios.common.Json;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;

import static com.google.common.base.Charsets.UTF_8;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * A class that is similar to {@code AtomicReference} but is backed by a file, so can be
 * persisted across a server restart.  Assumes the underlying type can be serialized by Jackson.
 *
 * Strangely, this is not actually atomic in the {@code AtomicReference} way; i.e. not threadsafe,
 * nor does it do CAS.
 */
public class PersistentAtomicReference<T> {

    private static final Logger log = LoggerFactory.getLogger(PersistentAtomicReference.class);

    private final Path filename;
    private final Path tempfilename;
    private final Object sync = new Object();

    private volatile T value;

    private PersistentAtomicReference(final Path filename, final JavaType javaType,
            final Supplier<? extends T> initialValue) throws IOException, InterruptedException {
        try {
            this.filename = filename.toAbsolutePath();
            this.tempfilename = filename.getFileSystem().getPath(this.filename.toString() + ".tmp");
            if (Files.exists(filename)) {
                final byte[] bytes = Files.readAllBytes(filename);
                if (bytes.length > 0) {
                    value = Json.read(bytes, javaType);
                } else {
                    value = initialValue.get();
                }
            } else {
                value = initialValue.get();
            }
        } catch (InterruptedIOException | ClosedByInterruptException e) {
            throw new InterruptedException(e.getMessage());
        }
    }

    /**
     * Set the reference to {@code newValue}.
     *
     * @param newValue The value to set.
     * @throws IOException If an error occurs working with the file on disk.
     * @throws InterruptedException If the thread is interrupted.
     */
    public void set(T newValue) throws IOException, InterruptedException {
        try {
            set0(newValue);
        } catch (InterruptedIOException | ClosedByInterruptException e) {
            throw new InterruptedException(e.getMessage());
        }
    }

    private void set0(final T newValue) throws IOException {
        log.debug("set: ({}) {}", filename, newValue);
        synchronized (sync) {
            final String json = Json.asPrettyStringUnchecked(newValue);
            log.debug("write: ({}) {}", tempfilename, json);
            Files.write(tempfilename, json.getBytes(UTF_8));
            log.debug("move: {} -> {}", tempfilename, filename);
            Files.move(tempfilename, filename, ATOMIC_MOVE, REPLACE_EXISTING);
            this.value = newValue;
        }
    }

    /**
     * Set the reference to {@code newValue}, and wraps {@link IOException}s in
     * {@link RuntimeException}s.
     *
     * @param newValue The value to set.
     * @throws InterruptedException If the thread is interrupted.
     */
    public void setUnchecked(T newValue) throws InterruptedException {
        try {
            set(newValue);
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Returns the value stored.
     *
     * @return The value.
     */
    public T get() {
        return value;
    }

    public static <T> PersistentAtomicReference<T> create(final Path filename, final TypeReference<T> typeReference,
            final Supplier<? extends T> initialValue) throws IOException, InterruptedException {

        return new PersistentAtomicReference<>(filename, Json.type(typeReference), initialValue);
    }

    public static <T> PersistentAtomicReference<T> create(final String filename,
            final TypeReference<T> typeReference, final Supplier<? extends T> initialValue)
            throws IOException, InterruptedException {
        return create(FileSystems.getDefault().getPath(filename), typeReference, initialValue);
    }

    public static <T> PersistentAtomicReference<T> create(final Path filename, final JavaType javaType,
            final Supplier<? extends T> initialValue) throws IOException, InterruptedException {
        return new PersistentAtomicReference<>(filename, javaType, initialValue);
    }

    public static <T> PersistentAtomicReference<T> create(final String filename, final JavaType javaType,
            final Supplier<? extends T> initialValue) throws IOException, InterruptedException {
        return create(FileSystems.getDefault().getPath(filename), javaType, initialValue);
    }

    @Override
    public String toString() {
        return "PersistentAtomicReference{" + "filename=" + filename + '}';
    }
}