/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.ignite.spi.discovery.tcp.ipfinder.sharedfs;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.IgniteInterruptedCheckedException;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.resources.LoggerResource;
import org.apache.ignite.spi.IgniteSpiConfiguration;
import org.apache.ignite.spi.IgniteSpiException;
import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinderAdapter;
/**
* Shared filesystem-based IP finder. <h1 class="header">Configuration</h1> <h2 class="header">Mandatory</h2> There are
* no mandatory configuration parameters. <h2 class="header">Optional</h2> <ul> <li>Path (see {@link
* #setPath(String)})</li> <li>Shared flag (see {@link #setShared(boolean)})</li> </ul> <p> If {@link #getPath()} is not
* provided, then {@link #DFLT_PATH} will be used and only local nodes will discover each other. To enable discovery
* over network you must provide a path to a shared directory explicitly. <p> The directory will contain empty files
* named like the following 192.168.1.136#1001. <p> Note that this finder is shared by default (see {@link
* org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder#isShared()}.
*/
public class TcpDiscoverySharedFsIpFinder extends TcpDiscoveryIpFinderAdapter {
/**
* Default path for discovering of local nodes (testing only). Note that this path is relative to {@code
* IGNITE_HOME/work} folder if {@code IGNITE_HOME} system or environment variable specified, otherwise it is
* relative to {@code work} folder under system {@code java.io.tmpdir} folder.
*
* @see org.apache.ignite.configuration.IgniteConfiguration#getWorkDirectory()
*/
public static final String DFLT_PATH = "disco/tcp";
/** Delimiter to use between address and port tokens in file names. */
public static final String DELIM = "#";
/** IPv6 colon delimiter. */
private static final String COLON_DELIM = ":";
/** IPv6 colon substitute. */
private static final String COLON_SUBST = "_";
/** Grid logger. */
@LoggerResource
private IgniteLogger log;
/** File-system path. */
private String path = DFLT_PATH;
/** Folder to keep items in. */
@GridToStringExclude
private File folder;
/** Warning guard. */
@GridToStringExclude
private final AtomicBoolean warnGuard = new AtomicBoolean();
/** Init guard. */
@GridToStringExclude
private final AtomicBoolean initGuard = new AtomicBoolean();
/** Init latch. */
@GridToStringExclude
private final CountDownLatch initLatch = new CountDownLatch(1);
/**
* Constructor.
*/
public TcpDiscoverySharedFsIpFinder() {
setShared(true);
}
/**
* Gets path.
*
* @return Shared path.
*/
public String getPath() {
return path;
}
/**
* Sets path.
*
* @param path Shared path.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = true)
public TcpDiscoverySharedFsIpFinder setPath(String path) {
this.path = path;
return this;
}
/**
* Initializes folder to work with.
*
* @return Folder.
* @throws org.apache.ignite.spi.IgniteSpiException If failed.
*/
private File initFolder() throws IgniteSpiException {
if (initGuard.compareAndSet(false, true)) {
if (path == null)
throw new IgniteSpiException("Shared file system path is null " +
"(it should be configured via setPath(..) configuration property).");
if (path.equals(DFLT_PATH) && warnGuard.compareAndSet(false, true))
U.warn(log, "Default local computer-only share is used by IP finder.");
try {
File tmp;
if (new File(path).exists())
tmp = new File(path);
else {
try {
tmp = U.resolveWorkDirectory(ignite.configuration().getWorkDirectory(), path, false);
}
catch (IgniteCheckedException e) {
throw new IgniteSpiException("Failed to resolve directory [path=" + path +
", exception=" + e.getMessage() + ']');
}
}
if (!tmp.isDirectory())
throw new IgniteSpiException("Failed to initialize shared file system path " +
"(path must point to folder): " + path);
if (!tmp.canRead() || !tmp.canWrite())
throw new IgniteSpiException("Failed to initialize shared file system path " +
"(path must be readable and writable): " + path);
folder = tmp;
}
finally {
initLatch.countDown();
}
}
else {
try {
U.await(initLatch);
}
catch (IgniteInterruptedCheckedException e) {
throw new IgniteSpiException("Thread has been interrupted.", e);
}
if (folder == null)
throw new IgniteSpiException("Failed to initialize shared file system folder (check logs for errors).");
}
return folder;
}
/** {@inheritDoc} */
@Override public Collection<InetSocketAddress> getRegisteredAddresses() throws IgniteSpiException {
initFolder();
Collection<InetSocketAddress> addrs = new LinkedList<>();
for (String fileName : folder.list())
if (!".svn".equals(fileName)) {
InetSocketAddress addr = null;
StringTokenizer st = new StringTokenizer(fileName, DELIM);
if (st.countTokens() == 2) {
String addrStr = st.nextToken();
String portStr = st.nextToken();
try {
int port = Integer.parseInt(portStr);
addr = new InetSocketAddress(denormalizeAddress(addrStr), port);
}
catch (IllegalArgumentException e) {
U.error(log, "Failed to parse file entry: " + fileName, e);
}
}
if (addr != null)
addrs.add(addr);
}
return Collections.unmodifiableCollection(addrs);
}
/** {@inheritDoc} */
@Override public void registerAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
assert !F.isEmpty(addrs);
initFolder();
try {
for (String name : distinctNames(addrs)) {
File file = new File(folder, name);
file.createNewFile();
}
}
catch (IOException e) {
throw new IgniteSpiException("Failed to create file.", e);
}
}
/** {@inheritDoc} */
@Override public void unregisterAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
assert !F.isEmpty(addrs);
initFolder();
try {
for (String name : distinctNames(addrs)) {
File file = new File(folder, name);
if (!file.delete())
throw new IgniteSpiException("Failed to delete file " + file.getName());
}
}
catch (SecurityException e) {
throw new IgniteSpiException("Failed to delete file.", e);
}
}
/**
* Returns set with unique names.
*
* @param addresses List of addresses.
* @return Set of addresses.
*/
private Iterable<String> distinctNames(Iterable<InetSocketAddress> addresses) {
Set<String> result = new HashSet<>();
for (InetSocketAddress address : addresses) {
result.add(name(address));
}
return result;
}
/**
* Creates file name for address.
*
* @param addr Node address.
* @return Name.
*/
private String name(InetSocketAddress addr) {
assert addr != null;
SB sb = new SB();
sb.a(normalizeAddress(addr.getAddress().getHostAddress()))
.a(DELIM)
.a(addr.getPort());
return sb.toString();
}
/**
* Normalizes the host address by substituting colon delimiter with underscore.
*
* @param hostAddress Host address.
* @return Normalized host address that can be safely used in file names.
*/
private String normalizeAddress(String hostAddress) {
return hostAddress.replaceAll(COLON_DELIM, COLON_SUBST);
}
/**
* Reverts changes done with {@link TcpDiscoverySharedFsIpFinder#normalizeAddress}.
*
* @param hostAddress Host address.
* @return Standard host address.
*/
private String denormalizeAddress(String hostAddress) {
return hostAddress.replaceAll(COLON_SUBST, COLON_DELIM);
}
/** {@inheritDoc} */
@Override public TcpDiscoverySharedFsIpFinder setShared(boolean shared) {
super.setShared(shared);
return this;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(TcpDiscoverySharedFsIpFinder.class, this);
}
}