Skip to content

Commit 7812a0e

Browse files
whitewhite
authored andcommitted
PoolMetrics 추가 (metrics() method)
PythonEmbedPool에 PoolMetrics 레코드와 metrics() 메서드를 추가합니다. - PoolMetrics: poolSize, activeCount, idleCount, minPool, maxPool, uptimeMs 레코드 - PythonEmbedPool: createdAt 필드와 metrics() 퍼블릭 메서드 추가 - PythonEmbedPoolTest: 스냅샷, busy/idle, scale-up, uptime 검증 4개 테스트 추가
1 parent d01e656 commit 7812a0e

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.howtis.pythonembed;
2+
3+
/**
4+
* A point-in-time snapshot of {@link PythonEmbedPool} metrics.
5+
*
6+
* <p>Obtained via {@link PythonEmbedPool#metrics()}. All values reflect
7+
* the pool's state at the moment of the call and may change immediately
8+
* afterward.
9+
*/
10+
public record PoolMetrics(
11+
int poolSize,
12+
int activeCount,
13+
int idleCount,
14+
int minPool,
15+
int maxPool,
16+
long uptimeMs) {
17+
}

python-embed-runtime/src/main/java/io/github/howtis/pythonembed/PythonEmbedPool.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class PythonEmbedPool implements AutoCloseable {
8080
private final Condition instanceAvailable = instanceLock.newCondition();
8181
private volatile long lastHealthCheckAt;
8282
private volatile boolean closed;
83+
private final long createdAt;
8384

8485
private final Map<String, CallbackHandler> callbackHandlers = new ConcurrentHashMap<>();
8586
private final Map<String, PushHandler> pushHandlers = new ConcurrentHashMap<>();
@@ -195,6 +196,7 @@ private PythonEmbedPool(Builder b) {
195196
this.healthCheckIntervalMs = b.healthCheckIntervalMs;
196197
this.options = b.options;
197198
this.onInstanceRemoved = b.onInstanceRemoved;
199+
this.createdAt = System.currentTimeMillis();
198200

199201
this.instances = new ConcurrentLinkedDeque<>();
200202
this.currentSize = new AtomicInteger(0);
@@ -781,6 +783,27 @@ public int maxPool() {
781783
return maxPool;
782784
}
783785

786+
/**
787+
* Returns a point-in-time snapshot of pool metrics.
788+
*
789+
* <p>The returned {@link PoolMetrics} reflects the pool's state
790+
* at the moment of the call. Values may change immediately afterward
791+
* due to concurrent activity.
792+
*
793+
* @return a snapshot of current pool metrics
794+
*/
795+
public PoolMetrics metrics() {
796+
int size = size();
797+
int active = activeCount();
798+
return new PoolMetrics(
799+
size,
800+
active,
801+
size - active,
802+
minPool,
803+
maxPool,
804+
System.currentTimeMillis() - createdAt);
805+
}
806+
784807
// ------------------------------------------------------------------
785808
// Lifecycle
786809
// ------------------------------------------------------------------

python-embed-runtime/src/test/java/io/github/howtis/pythonembed/PythonEmbedPoolTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,121 @@ void close_timeout_zero() throws Exception {
16941694
"close(0, SECONDS) should return quickly, elapsed=" + elapsed + "ms");
16951695
}
16961696

1697+
// ---- D4: Pool Metrics ----
1698+
1699+
@Test
1700+
void metrics_snapshotAfterBuild() throws Exception {
1701+
PythonEmbedPool pool = PythonEmbedPool.builder().minPool(2).maxPool(4).build();
1702+
try {
1703+
waitForPoolSize(pool, 2, 5000);
1704+
1705+
PoolMetrics m = pool.metrics();
1706+
assertEquals(2, m.poolSize());
1707+
assertEquals(0, m.activeCount());
1708+
assertEquals(2, m.idleCount());
1709+
assertEquals(2, m.minPool());
1710+
assertEquals(4, m.maxPool());
1711+
assertTrue(m.uptimeMs() >= 0, "uptimeMs should be non-negative");
1712+
} finally {
1713+
pool.close();
1714+
}
1715+
}
1716+
1717+
@Test
1718+
@Timeout(value = 30, unit = TimeUnit.SECONDS)
1719+
void metrics_idleCount_reflectsBusyInstances() throws Exception {
1720+
PythonEmbedPool pool = PythonEmbedPool.builder().maxPool(2).build();
1721+
try {
1722+
// Use exec instead of eval because Python eval() does not
1723+
// allow statement-level constructs like import + sleep.
1724+
CompletableFuture<Void> task = pool.exec(
1725+
"import time; time.sleep(3)");
1726+
1727+
// Poll until the task is acquired and instance becomes busy
1728+
long deadline = System.currentTimeMillis() + 5000;
1729+
while (pool.activeCount() == 0 && System.currentTimeMillis() < deadline) {
1730+
Thread.sleep(50);
1731+
}
1732+
1733+
PoolMetrics m = pool.metrics();
1734+
assertEquals(1, m.poolSize());
1735+
assertEquals(1, m.activeCount(), "Instance should be busy while task runs");
1736+
assertEquals(0, m.idleCount());
1737+
1738+
task.get(10, TimeUnit.SECONDS);
1739+
1740+
// Poll until instance is released
1741+
deadline = System.currentTimeMillis() + 5000;
1742+
while (pool.activeCount() > 0 && System.currentTimeMillis() < deadline) {
1743+
Thread.sleep(50);
1744+
}
1745+
m = pool.metrics();
1746+
assertEquals(1, m.poolSize());
1747+
assertEquals(0, m.activeCount(), "Instance should be idle after task completes");
1748+
assertEquals(1, m.idleCount());
1749+
} finally {
1750+
pool.close();
1751+
}
1752+
}
1753+
1754+
@Test
1755+
@Timeout(value = 30, unit = TimeUnit.SECONDS)
1756+
void metrics_idleCount_afterScaleUp() throws Exception {
1757+
PythonEmbedPool pool = PythonEmbedPool.builder().minPool(1).maxPool(2).build();
1758+
try {
1759+
// Submit two long-running tasks to trigger scale-up
1760+
CompletableFuture<Void> t1 = pool.exec(
1761+
"import time; time.sleep(3)");
1762+
// Poll until first task makes the instance busy
1763+
long deadline = System.currentTimeMillis() + 5000;
1764+
while (pool.activeCount() == 0 && System.currentTimeMillis() < deadline) {
1765+
Thread.sleep(50);
1766+
}
1767+
CompletableFuture<Void> t2 = pool.exec(
1768+
"import time; time.sleep(3)");
1769+
1770+
waitForPoolSize(pool, 2, 5000);
1771+
1772+
PoolMetrics m = pool.metrics();
1773+
assertEquals(2, m.poolSize());
1774+
assertEquals(2, m.activeCount(),
1775+
"Both instances should be busy during concurrent execution");
1776+
assertEquals(0, m.idleCount());
1777+
1778+
t1.get(10, TimeUnit.SECONDS);
1779+
t2.get(10, TimeUnit.SECONDS);
1780+
1781+
// Poll until both instances are released
1782+
deadline = System.currentTimeMillis() + 5000;
1783+
while (pool.activeCount() > 0 && System.currentTimeMillis() < deadline) {
1784+
Thread.sleep(50);
1785+
}
1786+
m = pool.metrics();
1787+
assertEquals(0, m.activeCount(),
1788+
"All instances should be idle after tasks complete");
1789+
assertEquals(2, m.idleCount());
1790+
} finally {
1791+
pool.close();
1792+
}
1793+
}
1794+
1795+
@Test
1796+
void metrics_uptimeMs_increases() throws Exception {
1797+
PythonEmbedPool pool = PythonEmbedPool.builder().maxPool(1).build();
1798+
try {
1799+
PoolMetrics m1 = pool.metrics();
1800+
Thread.sleep(200);
1801+
PoolMetrics m2 = pool.metrics();
1802+
1803+
assertTrue(m2.uptimeMs() > m1.uptimeMs(),
1804+
"uptimeMs should increase over time");
1805+
assertTrue(m2.uptimeMs() >= 200,
1806+
"uptimeMs should be at least the sleep duration");
1807+
} finally {
1808+
pool.close();
1809+
}
1810+
}
1811+
16971812
/**
16981813
* Waits for the pool to reach the given size, polling every 100ms.
16991814
*/

0 commit comments

Comments
 (0)