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

com.aoindustries.aoserv.master.ClusterHandler Maven / Gradle / Ivy

There is a newer version: 1.91.8
Show newest version
/*
 * aoserv-master - Master server for the AOServ Platform.
 * Copyright (C) 2009-2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020  AO Industries, Inc.
 *     [email protected]
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of aoserv-master.
 *
 * aoserv-master is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * aoserv-master is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with aoserv-master.  If not, see .
 */
package com.aoindustries.aoserv.master;

import com.aoindustries.aoserv.client.linux.Server;
import com.aoindustries.aoserv.client.master.User;
import com.aoindustries.aoserv.daemon.client.AOServDaemonConnector;
import com.aoindustries.collections.IntList;
import com.aoindustries.cron.CronDaemon;
import com.aoindustries.cron.CronJob;
import com.aoindustries.cron.Schedule;
import com.aoindustries.dbc.DatabaseConnection;
import com.aoindustries.util.Tuple3;
import com.aoindustries.util.logging.ProcessTimer;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The ClusterHandler maintains a mapping of virtual servers
 * to physical servers.  It updates its mapping every minute.
 *
 * @author  AO Industries, Inc.
 */
final public class ClusterHandler implements CronJob {

	private static final Logger logger = Logger.getLogger(ClusterHandler.class.getName());

	/**
	 * The maximum time for a processing pass.
	 */
	private static final long TIMER_MAX_TIME = 60L * 1000L; // Five minutes

	/**
	 * The interval in which the administrators will be reminded.
	 */
	private static final long TIMER_REMINDER_INTERVAL = 60L * 60L * 1000L; // One hour

	private static boolean started=false;

	public static void start() {
		synchronized(System.out) {
			if(!started) {
				System.out.print("Starting " + ClusterHandler.class.getSimpleName() + ": ");
				CronDaemon.addCronJob(new ClusterHandler(), logger);
				started=true;
				System.out.println("Done");
				// Run immediately to populate mapping on start-up
				MasterServer.executorService.submit(() -> {
					updateMappings();
				});
			}
		}
	}

	private ClusterHandler() {
	}

	/**
	 * Runs every minute
	 */
	private static final Schedule schedule = (int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) -> true;

	@Override
	public Schedule getSchedule() {
		return schedule;
	}

	@Override
	public int getThreadPriority() {
		return Thread.NORM_PRIORITY + 1;
	}

	@Override
	public void run(int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
		updateMappings();
	}

	public static class ClusterException extends IOException {

		private static final long serialVersionUID = 1L;

		public ClusterException() {
			super();
		}

		public ClusterException(String message) {
			super(message);
		}

		public ClusterException(Throwable cause) {
			super(cause);
		}

		public ClusterException(String message, Throwable cause) {
			super(message, cause);
		}
	}

	private static final Object mappingsLock = new Object();

	/**
	 * The set of virtual servers that have primary DRBD roles on a per physical
	 * server basis.
	 */
	private static Map> primaryMappings = Collections.emptyMap();

	/**
	 * The set of virtual servers that have secondary DRBD roles on a per physical
	 * server basis.
	 */
	private static Map> secondaryMappings = Collections.emptyMap();

	/**
	 * The set of virtual servers that have Xen auto start links on a per physical
	 * server basis.
	 */
	private static Map> autoMappings = Collections.emptyMap();

	private static void setMappings(
		Map> newPrimaryMappings,
		Map> newSecondaryMappings,
		Map> newAutoMappings
	) {
		synchronized(mappingsLock) {
			primaryMappings = newPrimaryMappings;
			secondaryMappings = newSecondaryMappings;
			autoMappings = newAutoMappings;
		}
	}

	public static int getPrimaryPhysicalServer(DatabaseConnection conn, RequestSource source, int virtualServer) throws IOException, SQLException {
		// Must be a cluster admin
		checkClusterAdmin(conn, source, "getPrimaryPhysicalServer");
		return getPrimaryPhysicalServer(virtualServer);
	}

	/**
	 * Gets the id of the physical server that is currently the primary for
	 * the virtual server.  If there is no primary (Secondary/Secondary role),
	 * will use the physical server that has Xen auto start configured.
	 */
	public static int getPrimaryPhysicalServer(int virtualServer) throws ClusterException {
		Integer virtualServerInt = virtualServer;
		int physicalServer = -1;
		boolean physicalServerFound = false;
		synchronized(mappingsLock) {
			for(Map.Entry> entry : primaryMappings.entrySet()) {
				if(entry.getValue().contains(virtualServerInt)) {
					if(physicalServerFound) throw new ClusterException("Virtual server #" + virtualServer + " primary found on more than one physical server");
					physicalServer = entry.getKey();
					physicalServerFound = true;
				}
			}
			if(!physicalServerFound) {
				for(Map.Entry> entry : autoMappings.entrySet()) {
					if(entry.getValue().contains(virtualServerInt)) {
						if(physicalServerFound) throw new ClusterException("Virtual server #" + virtualServer + " auto start link found on more than one physical server");
						physicalServer = entry.getKey();
						physicalServerFound = true;
					}
				}
			}
		}
		if(!physicalServerFound) throw new ClusterException("Virtual server #" + virtualServer + " primary not found on any physical server");
		return physicalServer;
	}

	public static int getSecondaryPhysicalServer(DatabaseConnection conn, RequestSource source, int virtualServer) throws IOException, SQLException {
		// Must be a cluster admin
		checkClusterAdmin(conn, source, "getSecondaryPhysicalServer");
		return getSecondaryPhysicalServer(virtualServer);
	}

	/**
	 * Gets the id of the physical server that is currently the secondary for
	 * the virtual server.  If there are two secondaries (Secondary/Secondary role),
	 * will use the physical server that does not have Xen auto start configured.
	 */
	public static int getSecondaryPhysicalServer(int virtualServer) throws ClusterException {
		Integer virtualServerInt = virtualServer;
		synchronized(mappingsLock) {
			// Find the set of all physical servers that have this as secondary
			Set physicalServers = new HashSet<>();
			for(Map.Entry> entry : secondaryMappings.entrySet()) {
				if(entry.getValue().contains(virtualServerInt)) {
					physicalServers.add(entry.getKey());
				}
			}
			// None found
			if(physicalServers.isEmpty()) {
				throw new ClusterException("Virtual server #" + virtualServer + " secondary not found on any physical server");
			}
			// If there is only one secondary, use it if not auto
			else if(physicalServers.size()==1) {
				Integer physicalServer1 = physicalServers.iterator().next();
				Set autoMappings1 = autoMappings.get(physicalServer1);
				boolean auto = autoMappings1!=null && autoMappings1.contains(virtualServer);
				if(auto) {
					throw new ClusterException("Virtual server #" + virtualServer + " secondary only found on physical server with auto start link: " + physicalServer1);
				}
				return physicalServer1;
			}
			// If two, choose the one that is not auto-start
			else if(physicalServers.size()==2) {
				Iterator iter = physicalServers.iterator();
				Integer physicalServer1 = iter.next();
				Integer physicalServer2 = iter.next();
				// Get the auto mappings for each
				Set autoMappings1 = autoMappings.get(physicalServer1);
				Set autoMappings2 = autoMappings.get(physicalServer2);
				// Find if has on auto
				boolean auto1 = autoMappings1!=null && autoMappings1.contains(virtualServer);
				boolean auto2 = autoMappings2!=null && autoMappings2.contains(virtualServer);
				// Resolve based on auto mappings
				if(auto1) {
					if(auto2) {
						// auto1 && auto2
						throw new ClusterException("Virtual server #" + virtualServer + " auto start link found on both physical servers: " + physicalServer1 + " and " + physicalServer2);
					} else {
						// auto1 && !auto2
						return physicalServer2;
					}
				} else {
					if(auto2) {
						// !auto1 && auto2
						return physicalServer1;
					} else {
						// !auto1 && !auto2
						throw new ClusterException("Virtual server #" + virtualServer + " auto start link not found on either physical server: " + physicalServer1 + " or " + physicalServer2);
					}
				}
			}
			// Error if more than two
			else {
				throw new ClusterException("Virtual server #" + virtualServer + " secondary found on more than two physical servers: " + physicalServers);
			}
		}
	}

	private static final Object updateMappingsLock = new Object();
	private static void updateMappings() {
		synchronized(updateMappingsLock) {
			try {
				try (
					ProcessTimer timer = new ProcessTimer(
						logger,
						ClusterHandler.class.getName(),
						"runCronJob",
						"ClusterHandler - Find Virtual Host Mapping",
						"Finding the current mapping of virtual servers onto physical servers",
						TIMER_MAX_TIME,
						TIMER_REMINDER_INTERVAL
					)
				) {
					MasterServer.executorService.submit(timer);

					// Query the servers in parallel
					final MasterDatabase database = MasterDatabase.getDatabase();
					IntList xenPhysicalServers = NetHostHandler.getEnabledXenPhysicalServers(database);
					Map,Set,Set>>> futures = new HashMap<>(xenPhysicalServers.size()*4/3+1);
					for(final Integer xenPhysicalServer : xenPhysicalServers) {
						futures.put(
							xenPhysicalServer,
							MasterServer.executorService.submit(() -> {
								// Try up to ten times
								for(int c=0;c<10;c++) {
									try {
										final int rootPackagePkey = PackageHandler.getIdForPackage(database, AccountHandler.getRootAccount());
										AOServDaemonConnector daemonConnnector = DaemonHandler.getDaemonConnector(database, xenPhysicalServer);
										// database.releaseConnection();
										// Get the DRBD states
										List drbdReports = Server.parseDrbdReport(daemonConnnector.getDrbdReport());
										Set primaryMapping = new HashSet<>(drbdReports.size()*4/3+1);
										Set secondaryMapping = new HashSet<>(drbdReports.size()*4/3+1);
										for(Server.DrbdReport drbdReport : drbdReports) {
											// Look for primary mappings
											if(
												drbdReport.getLocalRole()==Server.DrbdReport.Role.Primary
												&& (
													drbdReport.getRemoteRole()==Server.DrbdReport.Role.Unconfigured
													|| drbdReport.getRemoteRole()==Server.DrbdReport.Role.Secondary
													|| drbdReport.getRemoteRole()==Server.DrbdReport.Role.Unknown
												)
											) {
												primaryMapping.add(
													NetHostHandler.getHostForPackageAndName(
														database,
														rootPackagePkey,
														drbdReport.getResourceHostname()
													)
												);
											}
											// Look for secondary mappings
											if(
												drbdReport.getLocalRole()==Server.DrbdReport.Role.Secondary
												&& (
													drbdReport.getRemoteRole()==Server.DrbdReport.Role.Unconfigured
													|| drbdReport.getRemoteRole()==Server.DrbdReport.Role.Primary
													|| drbdReport.getRemoteRole()==Server.DrbdReport.Role.Unknown
												)
											) {
												secondaryMapping.add(
													NetHostHandler.getHostForPackageAndName(
														database,
														rootPackagePkey,
														drbdReport.getResourceHostname()
													)
												);
											}
										}
										// Get the auto-start list
										Set autoStartList = daemonConnnector.getXenAutoStartLinks();
										Set autoMapping = new HashSet<>(autoStartList.size()*4/3+1);
										for(String serverName : autoStartList) {
											autoMapping.add(
												NetHostHandler.getHostForPackageAndName(
													database,
													rootPackagePkey,
													serverName
												)
											);
										}
										return new Tuple3<>(
											primaryMapping,
											secondaryMapping,
											autoMapping
										);
									} catch(ThreadDeath td) {
										throw td;
									} catch(Throwable t) {
										if(c==9) throw t;
										logger.log(Level.SEVERE, null, t);
										try {
											Thread.sleep(2000);
										} catch(InterruptedException err) {
											logger.log(Level.WARNING, null, err);
										}
									}
								}
								throw new AssertionError("Exception should have been thrown when c==9");
							})
						);
					}
					Map> newPrimaryMappings = new HashMap<>(futures.size()*4/3+1);
					Map> newSecondaryMappings = new HashMap<>(futures.size()*4/3+1);
					Map> newAutoMappings = new HashMap<>(futures.size()*4/3+1);
					for(Map.Entry,Set,Set>>> future : futures.entrySet()) {
						Integer xenPhysicalServer = future.getKey();
						try {
							Tuple3,Set,Set> retVal = future.getValue().get(30, TimeUnit.SECONDS);
							newPrimaryMappings.put(xenPhysicalServer, retVal.getElement1());
							newSecondaryMappings.put(xenPhysicalServer, retVal.getElement2());
							newAutoMappings.put(xenPhysicalServer, retVal.getElement3());
						} catch(ThreadDeath TD) {
							throw TD;
						} catch(Throwable T) {
							logger.log(Level.SEVERE, "xenPhysicalServer="+xenPhysicalServer, T);
						}
					}
					setMappings(
						newPrimaryMappings,
						newSecondaryMappings,
						newAutoMappings
					);
				}
			} catch(ThreadDeath td) {
				throw td;
			} catch(Throwable t) {
				logger.log(Level.SEVERE, null, t);
			}
		}
	}

	public static boolean isClusterAdmin(DatabaseConnection conn, RequestSource source) throws IOException, SQLException {
		User mu=MasterServer.getUser(conn, source.getCurrentAdministrator());
		return mu!=null && mu.isClusterAdmin();
	}

	public static void checkClusterAdmin(DatabaseConnection conn, RequestSource source, String action) throws IOException, SQLException {
		if(!isClusterAdmin(conn, source)) throw new SQLException("Cluster administration not allowed, '"+action+"'");
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy