net.snowflake.client.core.HeartbeatBackground Maven / Gradle / Ivy
/*
* Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved.
*/
package net.snowflake.client.core;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* This class is a singleton which is running inside driver to heartbeat
* snowflake server for each connection
*/
public class HeartbeatBackground implements Runnable
{
private static HeartbeatBackground singleton = new HeartbeatBackground();
/**
* The logger.
*/
private static final SFLogger LOGGER =
SFLoggerFactory.getLogger(HeartbeatBackground.class);
// default master token validity (in seconds) is 4 hours
private long masterTokenValidityInSecs = 4 * 3600;
/**
* How often heartbeat executes is calculated based on master token validity
* and the headroom
*/
private long heartBeatIntervalInSecs = masterTokenValidityInSecs / 4;
// Scheduler handling the main heartbeat background
private ScheduledExecutorService scheduler = null;
// future for a scheduled heartbeat task
ScheduledFuture heartbeatFuture;
/**
* List of sessions to heartbeat. Use weak hash map so that if a session
* object is deleted and garbaged collected, it will be removed from the
* list so that we will not keep doing heartbeat for it. This is to take
* care of the case when some application does not close session before it
* goes out of scope.
*/
WeakHashMap sessions = new WeakHashMap<>();
// When is the last time heartbeat started
private long lastHeartbeatStartTimeInSecs = 0;
// Method to get the heartbeat instance
public static HeartbeatBackground getInstance()
{
return singleton;
}
/**
* private constructor so that no one can try to create one
*/
private HeartbeatBackground()
{
}
/**
* Method to add a session
*
* It will compare the master token validity and stored master token validity
* and if it is less, we will update the stored one and the heartbeat interval
* and reschedule heartbeat.
*
* This method is called when a session is created.
*
* @param session the session will be added
* @param masterTokenValidityInSecs time interval for which client need to
* check validity of master token with server
*/
synchronized protected void addSession(SFSession session,
long masterTokenValidityInSecs)
{
boolean requireReschedule = false;
// update heartbeat interval if master token validity has become smaller
if (masterTokenValidityInSecs < this.masterTokenValidityInSecs)
{
long oldMasterTokenValidityInSecs = this.masterTokenValidityInSecs;
long oldHeartbeatIntervalInSecs = this.heartBeatIntervalInSecs;
this.heartBeatIntervalInSecs = masterTokenValidityInSecs / 4;
// save master token validity
this.masterTokenValidityInSecs = masterTokenValidityInSecs;
LOGGER.debug("update heartbeat interval, master token validity"
+ " from {} to {}, heart beat interval from {} to {}",
oldMasterTokenValidityInSecs,
this.masterTokenValidityInSecs,
oldHeartbeatIntervalInSecs,
this.heartBeatIntervalInSecs);
// heartbeat rescheduling required
requireReschedule = true;
}
// add session to the list to be heartbeated
sessions.put(session, Boolean.TRUE);
/*
* Create scheduler if it is the first time. It uses a custom thread
* factory that will create daemon thread so that it will not block
* JVM from exiting.
*/
if (this.scheduler == null)
{
LOGGER.debug("create heartbeat thread pool");
this.scheduler = Executors.newScheduledThreadPool(
1,
new ThreadFactory()
{
@Override
public Thread newThread(Runnable runnable)
{
Thread thread =
Executors.defaultThreadFactory().newThread(runnable);
thread.setName("heartbeat (" + thread.getId() + ")");
thread.setDaemon(true);
return thread;
}
});
}
// schedule a heartbeat task if none exists
if (heartbeatFuture == null)
{
LOGGER.debug("schedule heartbeat task");
this.scheduleHeartbeat();
}
// or reschedule if the master token validity has been reduced (rare event)
else if (requireReschedule)
{
LOGGER.debug("Cancel existing heartbeat task");
// Cancel existing task if not started yet and reschedule
if (heartbeatFuture.cancel(false))
{
LOGGER.debug("Canceled existing heartbeat task, reschedule");
this.scheduleHeartbeat();
}
else
{
LOGGER.debug("Failed to cancel existing heartbeat task");
}
}
}
/**
* Method to remove a session. This is called when a session is closed.
* Notice that if a session is not closed but the session object goes out of
* scope, then this method will not be called. And then the session will be
* kept in the list of sessions to be heartbeated until the object gets
* garbage collected since we use weak reference to the object.
*
* @param session the session will be removed
*/
synchronized protected void removeSession(SFSession session)
{
sessions.remove(session);
}
/**
* Schedule the next heartbeat
*/
private void scheduleHeartbeat()
{
// elapsed time in seconds since the last heartbeat
long elapsedSecsSinceLastHeartBeat =
System.currentTimeMillis() / 1000 - lastHeartbeatStartTimeInSecs;
/*
* The initial delay for the new scheduling is 0 if the elapsed
* time is more than the heartbeat time interval, otherwise it is the
* difference between the heartbeat time interval and the elapsed time.
*/
long initialDelay = Math.max(heartBeatIntervalInSecs -
elapsedSecsSinceLastHeartBeat, 0);
LOGGER.debug(
"schedule heartbeat task with initial delay of {} seconds",
initialDelay);
// Creates and executes a periodic action to send heartbeats
this.heartbeatFuture =
this.scheduler.schedule(this, initialDelay, TimeUnit.SECONDS);
}
/**
* Run heartbeat: for each session send a heartbeat request and schedule
* next heartbeat as long as there are sessions left.
*
* Notice that the synchronization is only around the code that visits the
* global sessions map and the code that schedules
* next heartbeat, but not around the heartbeat calls for each session in
* order to minimize the chance of blocking the adding of a session by
* performing the heartbeats. This is because adding a session is called from
* JDBC connection creation call which directly affects application
* performance.
*/
@Override
public void run()
{
/**
* Remember current time as the heartbeat start time. This is used for
* calculating the delay for the next heartbeat.
*/
this.lastHeartbeatStartTimeInSecs = System.currentTimeMillis() / 1000;
Set sessionsToHeartbeat = new HashSet();
// synchronously get a copy of the sessions from the global list
synchronized (this)
{
sessionsToHeartbeat.addAll(sessions.keySet());
}
// heartbeat every session.
for (SFSession session : sessionsToHeartbeat)
{
try
{
session.heartbeat();
}
catch (Throwable ex)
{
LOGGER.error("heartbeat error - message=" + ex.getMessage(), ex);
}
}
/**
* The following is synchronized with the methods to add or remove a
* session so that we always make sure we have one heartbeat task scheduled
* when there is any session left.
*/
synchronized (this)
{
// schedule next heartbeat
if (sessions.size() > 0)
{
LOGGER.debug("schedule next heartbeat run");
scheduleHeartbeat();
}
else
{
LOGGER.debug("no need for heartbeat since no more sessions");
// no need to heartbeat if no more session
this.heartbeatFuture = null;
}
}
}
}