Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/com/password4j/Argon2Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ protected static String getUID(int memory, int iterations, int parallelism, int
return memory + "|" + iterations + "|" + parallelism + "|" + outputLength + "|" + type.ordinal() + "|" + version;
}

static void clearInstances()
{
INSTANCES.clear();
}

private static byte[] getInitialHashLong(byte[] initialHash, byte[] appendix)
{
byte[] initialHashLong = new byte[ARGON2_INITIAL_SEED_LENGTH];
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/password4j/BalloonHashingFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ private static String getUID(String algorithm, int spaceCost, int timeCost, int
return algorithm + '|' + spaceCost + '|' + timeCost + '|' + parallelism + '|' + delta;
}

static void clearInstances()
{
INSTANCES.clear();
}

protected static String toString(String algorithm, int spaceCost, int timeCost, int parallelism, int delta)
{
return "a=" + algorithm + ", s=" + spaceCost + ", t=" + timeCost + ", p=" + parallelism + ", d=" + delta;
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/password4j/Password.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ private Password()
//
}

/**
* Shuts down all internal worker thread pools used by parallel hashing functions
* (Argon2, Balloon) and clears their cached instances. This is intended for
* environments with a lifecycle shorter than the JVM — typically servlet
* containers, where it should be invoked from {@code ServletContextListener.contextDestroyed}
* to prevent thread/classloader leaks on webapp undeploy.
* <p>
* After this call, subsequent hashing requests will lazily re-create the workers.
* In a plain-JVM application it is not necessary to invoke this method: worker
* threads are daemons and are shut down automatically when the JVM exits.
*
* @since 1.8.5
*/
public static void shutdown()
{
Utils.shutdownExecutors();
}

/**
* Starts to hash the given plain text password.
* <p>
Expand Down
56 changes: 53 additions & 3 deletions src/main/java/com/password4j/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -55,6 +58,10 @@ class Utils

private static final ThreadGroup THREAD_GROUP = new ThreadGroup("Password4j Workers");

private static final Set<ExecutorService> EXECUTORS = Collections.newSetFromMap(new ConcurrentHashMap<>());

private static volatile Thread shutdownHook;

static
{
Arrays.fill(FROM_BASE64, -1);
Expand Down Expand Up @@ -669,12 +676,55 @@ static ExecutorService createExecutorService()
return thread;
});

addShutdownHook(executorService);
EXECUTORS.add(executorService);
ensureShutdownHook();
return executorService;
}

static void addShutdownHook(ExecutorService executorService)
private static synchronized void ensureShutdownHook()
{
Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow, "password4j-shutdownhook"));
if (shutdownHook != null)
{
return;
}
Thread hook = new Thread(() -> {
for (ExecutorService service : EXECUTORS)
{
service.shutdownNow();
}
EXECUTORS.clear();
}, "password4j-shutdownhook");
try
{
Runtime.getRuntime().addShutdownHook(hook);
shutdownHook = hook;
}
catch (IllegalStateException e)
{
// JVM is already shutting down; no hook needed.
}
}

static synchronized void shutdownExecutors()
{
for (ExecutorService service : EXECUTORS)
{
service.shutdownNow();
}
EXECUTORS.clear();
Argon2Function.clearInstances();
BalloonHashingFunction.clearInstances();
if (shutdownHook != null)
{
try
{
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
catch (IllegalStateException e)
{
// JVM is shutting down; the hook is running or finished. Nothing to do.
}
shutdownHook = null;
}
}
}
47 changes: 47 additions & 0 deletions src/test/com/password4j/PasswordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1272,4 +1272,51 @@ public PermissionCollection getPermissions(CodeSource codesource)
return;
}
}

@Test
public void shutdownStopsWorkerThreadsAndPermitsReuse()
{
// Prime the pool so worker threads exist.
Argon2Function argon2 = Argon2Function.getInstance(256, 1, 2, 32, Argon2.ID);
Password.hash("password").with(argon2);
assertTrue("worker threads should exist before shutdown", workerThreadCount() > 0);

Password.shutdown();

// After shutdown the workers should wind down quickly (daemon interrupt).
long deadline = System.currentTimeMillis() + 2000;
while (workerThreadCount() > 0 && System.currentTimeMillis() < deadline)
{
try
{
Thread.sleep(20);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
break;
}
}
assertEquals("worker threads must be terminated after shutdown", 0, workerThreadCount());

// Hashing must still work afterwards: a fresh pool is created lazily.
Argon2Function argon2Again = Argon2Function.getInstance(256, 1, 2, 32, Argon2.ID);
Hash hash = Password.hash("password").with(argon2Again);
assertNotNull(hash.getResult());
assertTrue("worker threads should exist again after reuse", workerThreadCount() > 0);
Password.shutdown();
}

private static int workerThreadCount()
{
int count = 0;
for (Thread thread : Thread.getAllStackTraces().keySet())
{
if (thread.isAlive() && thread.getName().startsWith("password4j-worker-"))
{
count++;
}
}
return count;
}
}