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

org.zalando.boot.etcd.EtcdClient Maven / Gradle / Ivy

There is a newer version: 2.3
Show newest version
/*
 * 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 https://coreos.com
 *      /etcd/docs/2.1.0/api.html
 */
@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
	 *            true if child nodes should be returned,
	 *            false 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 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 -1 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 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 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
	 *            true if the existing node should be updated,
	 *            false 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 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
	 *            true if the existing node should be updated,
	 *            false 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 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 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 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 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 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 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 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 locations = new ArrayList();

			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 execute(UriComponentsBuilder uriTemplate, HttpMethod method,
			MultiValueMap requestData, Class responseType) throws EtcdException {
		long startTimeMillis = System.currentTimeMillis();
		int retry = -1;

		ResourceAccessException lastException = null;
		do {
			lastException = null;

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

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

			try {
				ResponseEntity 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;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy