org.zalando.boot.etcd.EtcdClient.java Source code

Java tutorial

Introduction

Here is the source code for org.zalando.boot.etcd.EtcdClient.java

Source

/*
 * Copyright 2015 Zalando SE
 *
 * 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 org.zalando.boot.etcd;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.apachecommons.CommonsLog;

/**
 * A service that encapsulates the communication with an etcd cluster.
 * 
 * @see <a href="https://coreos.com/etcd/docs/2.1.0/api.html">https://coreos.com
 *      /etcd/docs/2.1.0/api.html</a>
 */
@CommonsLog
public class EtcdClient implements InitializingBean, DisposableBean {

    /**
     * base path
     */
    private static final String BASE_PATH = "{location}/v2";

    /**
     * key space containing all nodes with key-value pairs
     */
    private static final String KEYSPACE = BASE_PATH + "/keys";

    /**
     * member space
     */
    private static final String MEMBERSPACE = BASE_PATH + "/members";

    /**
     * request converter
     */
    private AllEncompassingFormHttpMessageConverter requestConverter = new AllEncompassingFormHttpMessageConverter();

    /**
     * response converter
     */
    private MappingJackson2HttpMessageConverter responseConverter = new MappingJackson2HttpMessageConverter();

    /**
     * request factory
     */
    @Getter
    @Setter
    private ClientHttpRequestFactory requestFactory;

    /**
     * template.
     */
    private RestTemplate template;

    /**
     * number of retries
     */
    @Getter
    @Setter
    private int retryCount = 0;

    /**
     * duration of retries
     */
    @Getter
    @Setter
    private int retryDuration = 0;

    /**
     * locations
     */
    private String[] locations;

    /**
     * indicates whether the location updater is enabled
     */
    private boolean locationUpdaterEnabled = true;

    /**
     * current location
     */
    private int locationIndex = 0;

    /**
     * location updater
     */
    private ScheduledExecutorService locationUpdater = Executors.newScheduledThreadPool(1, new ThreadFactory() {

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "etcd-location-updater");
            t.setDaemon(true);
            return t;
        }
    });

    /**
     * Creates a new EtcdClient.
     */
    public EtcdClient() {
        super();
    }

    /**
     * Creates a new EtcdClient with the given location.
     * 
     * @param location
     *            the location
     */
    public EtcdClient(String location) {
        this.locations = new String[] { location };
    }

    /**
     * Creates a new EtcdClient with the given locations.
     * 
     * @param locations
     *            the locations
     */
    public EtcdClient(String[] locations) {
        this.locations = locations;
    }

    public boolean isLocationUpdaterEnabled() {
        return locationUpdaterEnabled;
    }

    public void setLocationUpdaterEnabled(boolean value) {
        this.locationUpdaterEnabled = value;
    }

    /**
     * @param value
     *            the locations
     */
    public void setLocations(String[] value) {
        this.locations = value == null ? new String[0] : value;
    }

    /**
     * @return the locations
     */
    public String[] getLocations() {
        return locations;
    }

    /**
     * @return the current location
     */
    protected String getCurrentLocation() {
        return locations[locationIndex];
    }

    /**
     * Returns the node with the given key from etcd.
     * 
     * @param key
     *            the node's key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse get(String key) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        return execute(builder, HttpMethod.GET, null, EtcdResponse.class);
    }

    /**
     * Returns the node with the given key from etcd.
     * 
     * @param key
     *            the node's key
     * @param recursive
     *            <code>true</code> if child nodes should be returned,
     *            <code>false</code> otherwise
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse get(String key, boolean recursive) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("recursive", recursive);

        return execute(builder, HttpMethod.GET, null, EtcdResponse.class);
    }

    /**
     * Sets the value of the node with the given key in etcd. Any previously
     * existing key-value pair is returned as prevNode in the etcd response.
     * 
     * @param key
     *            the node's key
     * @param value
     *            the node's value
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse put(final String key, final String value) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Sets the value of the node with the given key in etcd.
     * 
     * @param key
     *            the node's key
     * @param value
     *            the node's value
     * @param ttl
     *            the node's time-to-live or <code>-1</code> to unset existing
     *            ttl
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse put(String key, String value, int ttl) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("ttl", ttl == -1 ? "" : ttl);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Deletes the node with the given key from etcd.
     * 
     * @param key
     *            the node's key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse delete(final String key) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        return execute(builder, HttpMethod.DELETE, null, EtcdResponse.class);
    }

    /**
     * Creates a new node with the given key-value pair under the node with the
     * given key.
     * 
     * @param key
     *            the directory node's key
     * @param value
     *            the value of the created node
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse create(final String key, final String value) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.POST, payload, EtcdResponse.class);
    }

    /**
     * Atomically creates or updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param prevExist
     *            <code>true</code> if the existing node should be updated,
     *            <code>false</code> of the node should be created
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(final String key, final String value, boolean prevExist)
            throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("prevExist", prevExist);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically creates or updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param ttl
     *            the time-to-live
     * @param prevExist
     *            <code>true</code> if the existing node should be updated,
     *            <code>false</code> of the node should be created
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(final String key, final String value, int ttl, boolean prevExist)
            throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("ttl", ttl == -1 ? "" : ttl);
        builder.queryParam("prevExist", prevExist);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param prevIndex
     *            the modified index of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(String key, String value, int prevIndex) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("prevIndex", prevIndex);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param ttl
     *            the time-to-live
     * @param prevIndex
     *            the modified index of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(String key, String value, int ttl, int prevIndex) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("ttl", ttl == -1 ? "" : ttl);
        builder.queryParam("prevIndex", prevIndex);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param prevValue
     *            the previous value of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(String key, String value, String prevValue) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("prevValue", prevValue);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically updates a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @param ttl
     *            the time-to-live
     * @param prevValue
     *            the previous value of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndSwap(String key, String value, int ttl, String prevValue) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("ttl", ttl == -1 ? "" : ttl);
        builder.queryParam("prevValue", prevValue);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("value", value);

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Atomically deletes a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param prevIndex
     *            the modified index of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndDelete(final String key, int prevIndex) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("prevIndex", prevIndex);

        return execute(builder, HttpMethod.DELETE, null, EtcdResponse.class);
    }

    /**
     * Atomically deletes a key-value pair in etcd.
     * 
     * @param key
     *            the key
     * @param prevValue
     *            the previous value of the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse compareAndDelete(final String key, String prevValue) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("prevValue", prevValue);

        return execute(builder, HttpMethod.DELETE, null, EtcdResponse.class);
    }

    /**
     * Creates a directory node in etcd.
     * 
     * @param key
     *            the key
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse putDir(final String key) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("dir", "true");

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    /**
     * Creates a directory node in etcd.
     * 
     * @param key
     *            the key
     * @param ttl
     *            the time-to-live
     * @return the response from etcd with the node
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdResponse putDir(String key, int ttl) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);

        MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(1);
        payload.set("dir", "true");
        payload.set("ttl", ttl == -1 ? "" : String.valueOf(ttl));

        return execute(builder, HttpMethod.PUT, payload, EtcdResponse.class);
    }

    public EtcdResponse deleteDir(String key) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("dir", "true");

        return execute(builder, HttpMethod.DELETE, null, EtcdResponse.class);
    }

    public EtcdResponse deleteDir(String key, boolean recursive) throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(KEYSPACE);
        builder.pathSegment(key);
        builder.queryParam("recursive", recursive);

        return execute(builder, HttpMethod.DELETE, null, EtcdResponse.class);
    }

    /**
     * Returns a representation of all members in the etcd cluster.
     * 
     * @return the members
     * @throws EtcdException
     *             in case etcd returned an error
     */
    public EtcdMemberResponse listMembers() throws EtcdException {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(MEMBERSPACE);
        return execute(builder, HttpMethod.GET, null, EtcdMemberResponse.class);
    }

    /**
     * {@inheritDoc}
     * 
     * @see InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        if (this.requestFactory == null) {
            SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
            requestFactory.setConnectTimeout(1000);
            requestFactory.setReadTimeout(3000);
            this.requestFactory = requestFactory;
        }

        template = new RestTemplate(this.requestFactory);
        template.setMessageConverters(Arrays.asList(requestConverter, responseConverter));

        if (locationUpdaterEnabled) {
            Runnable worker = new Runnable() {
                @Override
                public void run() {
                    updateMembers();
                }
            };
            locationUpdater.scheduleAtFixedRate(worker, 5000, 5000, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see DisposableBean#destroy()
     */
    @Override
    public void destroy() throws Exception {
        locationUpdater.shutdownNow();
    }

    /**
     * Updates the locations of the etcd cluster members.
     */
    private void updateMembers() {
        try {
            List<String> locations = new ArrayList<String>();

            EtcdMemberResponse response = listMembers();
            EtcdMember[] members = response.getMembers();

            for (EtcdMember member : members) {
                String[] clientUrls = member.getClientURLs();
                if (clientUrls != null) {
                    for (String clientUrl : clientUrls) {
                        try {
                            String version = template.getForObject(clientUrl + "/version", String.class);
                            if (version == null) {
                                locations.add(clientUrl);
                            }
                        } catch (RestClientException e) {
                            log.debug("ignoring URI " + clientUrl + " because of error.", e);
                        }
                    }
                }
            }

            if (!locations.isEmpty()) {
                this.locations = locations.toArray(new String[locations.size()]);
            } else {
                log.debug("not updating locations because no location is found");
            }
        } catch (EtcdException e) {
            log.error("Could not update etcd cluster member.", e);
        }
    }

    /**
     * Executes the given method on the given location using the given request
     * data.
     * 
     * @param uri
     *            the location
     * @param method
     *            the HTTP method
     * @param requestData
     *            the request data
     * @return the etcd response
     * @throws EtcdException
     *             in case etcd returned an error
     */
    private <T> T execute(UriComponentsBuilder uriTemplate, HttpMethod method,
            MultiValueMap<String, String> requestData, Class<T> responseType) throws EtcdException {
        long startTimeMillis = System.currentTimeMillis();
        int retry = -1;

        ResourceAccessException lastException = null;
        do {
            lastException = null;

            URI uri = uriTemplate.buildAndExpand(locations[locationIndex]).toUri();

            RequestEntity<MultiValueMap<String, String>> requestEntity = new RequestEntity<>(requestData, null,
                    method, uri);

            try {
                ResponseEntity<T> responseEntity = template.exchange(requestEntity, responseType);
                return responseEntity.getBody();
            } catch (HttpStatusCodeException e) {
                EtcdError error = null;
                try {
                    error = responseConverter.getObjectMapper().readValue(e.getResponseBodyAsByteArray(),
                            EtcdError.class);
                } catch (IOException ex) {
                    error = null;
                }
                throw new EtcdException(error, "Failed to execute " + requestEntity + ".", e);
            } catch (ResourceAccessException e) {
                log.debug("Failed to execute " + requestEntity + ", retrying if possible.", e);

                if (locationIndex == locations.length - 1) {
                    locationIndex = 0;
                } else {
                    locationIndex++;
                }
                lastException = e;
            }
        } while (retry <= retryCount && System.currentTimeMillis() - startTimeMillis < retryDuration);

        if (lastException != null) {
            throw lastException;
        } else {
            return null;
        }
    }
}