/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.pg; import com.foundationdb.sql.server.CacheCounters; import com.foundationdb.sql.server.ServerServiceRequirements; import com.foundationdb.sql.server.ServerStatementCache; import com.foundationdb.server.error.InvalidPortException; import com.foundationdb.server.service.metrics.LongMetric; import com.foundationdb.server.service.monitor.MonitorStage; import com.foundationdb.server.service.monitor.ServerMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.security.Principal; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; /** The PostgreSQL server. * Listens of a given port and spawns <code>PostgresServerConnection</code> threads * to process requests. * Also keeps global state for shutdown and inter-connection communication like cancel. */ public class PostgresServer implements Runnable, ServerMonitor { public static final String COMMON_PROPERTIES_PREFIX = "fdbsql.sql."; public static final String SERVER_PROPERTIES_PREFIX = "fdbsql.postgres."; protected static final String SERVER_TYPE = "Postgres"; private static final String THREAD_NAME_PREFIX = "PostgresServer_Accept-"; // Port is appended private static final String BYTES_IN_METRIC_NAME = "PostgresBytesIn"; private static final String BYTES_OUT_METRIC_NAME = "PostgresBytesOut"; protected static enum AuthenticationType { NONE, CLEAR_TEXT, MD5, GSS, JAAS }; private final Properties properties; private final int port; private final String host; private final ServerServiceRequirements reqs; private ServerSocket socket = null; private volatile boolean running = false; private volatile long startTimeMillis; private boolean listening = false; private int nconnections = 0; private Map<Integer,PostgresServerConnection> connections = new HashMap<>(); private Thread thread; // AIS-dependent state private volatile int statementCacheCapacity; private final Map<ObjectLongPair,ServerStatementCache<PostgresStatement>> statementCaches = new HashMap<>(); // key and aisGeneration // end AIS-dependent state private volatile Date overrideCurrentTime; private final CacheCounters cacheCounters = new CacheCounters(); private AuthenticationType authenticationType; private Subject gssLogin; private String jaasConfigName; private Class<? extends Principal> jaasUserClass; private Collection<Class<? extends Principal>> jaasRoleClasses; private final int slowLimit; private final int hardLimit; private static final Logger logger = LoggerFactory.getLogger(PostgresServer.class); public PostgresServer(ServerServiceRequirements reqs) { this.reqs = reqs; properties = reqs.config().deriveProperties(COMMON_PROPERTIES_PREFIX); properties.putAll(reqs.config().deriveProperties(SERVER_PROPERTIES_PREFIX)); String portString = properties.getProperty("port"); port = Integer.parseInt(portString); if (port <= 0) throw new InvalidPortException(port); host = properties.getProperty("host"); String capacityString = properties.getProperty("statementCacheCapacity"); statementCacheCapacity = Integer.parseInt(capacityString); slowLimit = Integer.parseInt(properties.getProperty("connection_slow_limit", "250")); hardLimit = Integer.parseInt(properties.getProperty("connection_hard_limit", "500")); } public Properties getProperties() { return properties; } public int getPort() { return port; } public String getHost() { return host; } /** Called from the (Main's) main thread to start a server running in its own thread. */ public void start() { running = true; startTimeMillis = System.currentTimeMillis(); thread = new Thread(this, THREAD_NAME_PREFIX + getPort()); thread.start(); } /** Called from the main thread to shutdown a server. */ public void stop() { ServerSocket socket; synchronized(this) { // Service might shutdown before we've even got server socket created. running = listening = false; socket = this.socket; } if (socket != null) { // Can only wake up by closing socket inside whose accept() we are blocked. try { socket.close(); } catch (IOException ex) { } } Collection<PostgresServerConnection> conns; synchronized (this) { // Get a copy so they can remove themselves from stop(). conns = new ArrayList<>(connections.values()); } for (PostgresServerConnection connection : conns) { connection.stop(); } if (thread != null) { try { // Wait a bit, but don't hang up shutdown if thread is wedged. thread.join(500); if (thread.isAlive()) logger.warn("Server still running."); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } thread = null; } } @Override public void run() { computeAuthenticationType(); logger.info("Starting Postgres server listening on {}:{} with authentication {}", host, port, authenticationType); Random rand = new Random(); LongMetric bytesInMetric = null, bytesOutMetric = null; try { bytesInMetric = reqs.metricsService().addLongMetric(BYTES_IN_METRIC_NAME); bytesOutMetric = reqs.metricsService().addLongMetric(BYTES_OUT_METRIC_NAME); reqs.monitor().registerServerMonitor(this); synchronized(this) { if (!running) return; // 50 here was taken from the shorter new ServerSocket(port) socket = new ServerSocket(port, 50, InetAddress.getByName(host)); listening = true; } while (running) { Socket sock = socket.accept(); // If we're running too many connections, slow down... if (connections.size() > hardLimit) { logger.warn("Connection hard limit exceeded, wait for connections to close..."); do { try { Thread.sleep(10); } catch (InterruptedException ex) { } } while (connections.size() > hardLimit); } else if (connections.size() > slowLimit) { logger.warn("Connection slowdown limit exceeded, delaying connection start..."); try { Thread.sleep(10); } catch (InterruptedException ex) { } } int sessionId = reqs.monitor().allocateSessionId(); int secret = rand.nextInt(); PostgresServerConnection connection = new PostgresServerConnection(this, sock, sessionId, secret, bytesInMetric, bytesOutMetric, reqs); nconnections++; connections.put(sessionId, connection); connection.start(); } } catch (Exception ex) { if (running) logger.warn("Error in server", ex); } finally { if (socket != null) { try { socket.close(); } catch (IOException ex) { } } reqs.monitor().deregisterServerMonitor(this); reqs.metricsService().removeMetric(bytesOutMetric); reqs.metricsService().removeMetric(bytesInMetric); running = false; } } public synchronized boolean isListening() { return listening; } public synchronized PostgresServerConnection getConnection(int sessionId) { return connections.get(sessionId); } public synchronized void removeConnection(int sessionId) { connections.remove(sessionId); } public synchronized Collection<PostgresServerConnection> getConnections() { return new ArrayList<>(connections.values()); } public String getSqlString(int sessionId) { return getConnection(sessionId).getSessionMonitor().getCurrentStatement(); } public String getRemoteAddress(int sessionId) { return getConnection(sessionId).getSessionMonitor().getRemoteAddress(); } public void cancelQuery(int sessionId) { getConnection(sessionId).cancelQuery(null, "JMX"); } public void killConnection(int sessionId) { PostgresServerConnection conn = getConnection(sessionId); conn.cancelQuery("your session being disconnected", "JMX"); conn.waitAndStop(); } void cleanStatementCaches(long newGeneration) { Set<Long> activeGenerations = reqs.dxl().ddlFunctions().getActiveGenerations(); logger.debug("Cleaning statement caches except {} (now {})", activeGenerations, newGeneration); synchronized (statementCaches) { Iterator<Map.Entry<ObjectLongPair,ServerStatementCache<PostgresStatement>>> it = statementCaches.entrySet().iterator(); while(it.hasNext()) { Map.Entry<ObjectLongPair,ServerStatementCache<PostgresStatement>> entry = it.next(); if (!activeGenerations.contains(entry.getKey().longVal)) { entry.getValue().invalidate(); // It may be a while before a connection gets a new one. it.remove(); } } } } /** This is the version for use by connections. */ public ServerStatementCache<PostgresStatement> getStatementCache(Object key, long aisGeneration) { if (statementCacheCapacity <= 0) return null; ObjectLongPair fullKey = new ObjectLongPair(key, aisGeneration); ServerStatementCache<PostgresStatement> statementCache; synchronized (statementCaches) { statementCache = statementCaches.get(fullKey); if (statementCache == null) { // No cache => recent DDL, reasonable time to do a little cleaning cleanStatementCaches(aisGeneration); statementCache = new ServerStatementCache<>(cacheCounters, statementCacheCapacity); statementCaches.put(fullKey, statementCache); } } return statementCache; } public int getStatementCacheCapacity() { return statementCacheCapacity; } public void setStatementCacheCapacity(int capacity) { statementCacheCapacity = capacity; synchronized (statementCaches) { for (ServerStatementCache<PostgresStatement> statementCache : statementCaches.values()) { statementCache.setCapacity(capacity); } if (capacity <= 0) { statementCaches.clear(); } } } public int getStatementCacheHits() { return cacheCounters.getHits(); } public int getStatementCacheMisses() { return cacheCounters.getMisses(); } public void resetStatementCache() { synchronized (statementCaches) { cacheCounters.reset(); for (ServerStatementCache<PostgresStatement> statementCache : statementCaches.values()) { statementCache.reset(); } } } // used for testing public Set<Integer> getCurrentSessions() { return new HashSet<>(connections.keySet()); } /** For testing, set the server's idea of the current time. */ public void setOverrideCurrentTime(Date overrideCurrentTime) { this.overrideCurrentTime = overrideCurrentTime; } public Date getOverrideCurrentTime() { return overrideCurrentTime; } public AuthenticationType getAuthenticationType() { return authenticationType; } protected void computeAuthenticationType() { if (properties.getProperty("gssConfigName") != null) { authenticationType = AuthenticationType.GSS; } else if (properties.getProperty("jaas.configName") != null) { authenticationType = AuthenticationType.JAAS; jaasConfigName = properties.getProperty("jaas.configName"); String cprop = properties.getProperty("jaas.userClass"); if (cprop != null) { try { jaasUserClass = Class.forName(cprop).asSubclass(Principal.class); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException("Invalid jaas.userClass", ex); } } cprop = properties.getProperty("jaas.roleClasses"); if (cprop != null) { jaasRoleClasses = new ArrayList<>(); try { for (String c : cprop.split(",")) { jaasRoleClasses.add(Class.forName(c.trim()).asSubclass(Principal.class)); } } catch (ClassNotFoundException ex) { throw new IllegalArgumentException("Invalid jaas.roleClasses", ex); } } } else { String login = properties.getProperty("login", "none"); if (login.equals("none")) { authenticationType = AuthenticationType.NONE; } else if (login.equals("password")) { authenticationType = AuthenticationType.CLEAR_TEXT; } else if (login.equals("md5")) { authenticationType = AuthenticationType.MD5; } else { throw new IllegalArgumentException("Invalid login property: " + login); } } } /** Login to the KDC as the service for this server (using a keytab) * and return the <code>Subject</code> that can then be used to * authenticate a client. */ public synchronized Subject getGSSLogin() throws LoginException { if (gssLogin == null) { LoginContext lc = new LoginContext(properties.getProperty("gssConfigName")); lc.login(); gssLogin = lc.getSubject(); } return gssLogin; } public String getJaasConfigName() { return jaasConfigName; } public Class<? extends Principal> getJaasUserClass() { return jaasUserClass; } public Collection<Class<? extends Principal>> getJaasRoleClasses() { return jaasRoleClasses; } /* ServerMonitor */ @Override public String getServerType() { return SERVER_TYPE; } @Override public int getLocalPort() { if (listening) return port; else return -1; } @Override public String getLocalHost() { if (listening) return host; else return null; } @Override public long getStartTimeMillis() { return startTimeMillis; } @Override public int getSessionCount() { return nconnections; } }