Skip to content

Commit c0d5747

Browse files
authored
Merge pull request #4260 from 1c-syntax/perf/jlanguagetool-pool-cap
perf(diagnostics): ограничение пула JLanguageTool
2 parents 61640a0 + b13b486 commit c0d5747

3 files changed

Lines changed: 199 additions & 2 deletions

File tree

src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/typo/JLanguageToolPool.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,32 @@
2222
package com.github._1c_syntax.bsl.languageserver.diagnostics.typo;
2323

2424
import com.github._1c_syntax.bsl.languageserver.utils.AbstractObjectPool;
25-
import lombok.AllArgsConstructor;
2625
import org.languagetool.JLanguageTool;
2726
import org.languagetool.Language;
2827
import org.languagetool.rules.Rule;
2928

3029
import java.util.function.Predicate;
3130

32-
@AllArgsConstructor
31+
/**
32+
* Пул экземпляров {@link JLanguageTool} для одного языка.
33+
* <p>
34+
* Пул ограничен: каждый экземпляр при создании загружает собственную копию
35+
* полного набора правил языка (десятки мегабайт на инстанс, отключение правил
36+
* память не освобождает), поэтому рост пула до числа потоков анализа
37+
* недопустим по памяти. При исчерпании лимита {@link #checkOut()} ждёт
38+
* возврата экземпляра.
39+
*/
3340
public class JLanguageToolPool extends AbstractObjectPool<JLanguageTool> {
3441

42+
private static final int MAX_POOL_SIZE = 4;
43+
3544
private final Language language;
3645

46+
public JLanguageToolPool(Language language) {
47+
super(Math.min(MAX_POOL_SIZE, Runtime.getRuntime().availableProcessors()));
48+
this.language = language;
49+
}
50+
3751
@Override
3852
protected JLanguageTool create() {
3953
JLanguageTool languageTool = new JLanguageTool(language);

src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/AbstractObjectPool.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,53 @@
5050

5151
/**
5252
* Generic object pool.
53+
* <p>
54+
* Может быть ограничен по количеству создаваемых объектов: при достижении
55+
* лимита {@link #checkOut()} блокируется до возврата объекта через
56+
* {@link #checkIn(Object)}.
5357
*
5458
* @param <T> Type T of Object in the Pool
5559
*/
5660
public abstract class AbstractObjectPool<T> {
5761

5862
private final Set<T> available = new HashSet<>();
5963
private final Set<T> inUse = new HashSet<>();
64+
private final int maxSize;
65+
66+
protected AbstractObjectPool() {
67+
this(Integer.MAX_VALUE);
68+
}
69+
70+
/**
71+
* @param maxSize максимальное суммарное количество объектов пула
72+
* (доступных и выданных); не меньше 1.
73+
*/
74+
protected AbstractObjectPool(int maxSize) {
75+
if (maxSize < 1) {
76+
throw new IllegalArgumentException("maxSize must be >= 1, got: " + maxSize);
77+
}
78+
this.maxSize = maxSize;
79+
}
6080

6181
protected abstract T create();
6282

6383
/**
6484
* Checkout object from pool.
85+
* <p>
86+
* Если пул пуст и лимит объектов исчерпан — ждёт возврата объекта.
87+
* Прерывание потока во время ожидания не прерывает выдачу: флаг прерывания
88+
* восстанавливается, а прерванный поток получает объект сверх лимита
89+
* (пул может временно превысить {@code maxSize} на число прерванных ожидающих).
6590
*/
6691
public synchronized T checkOut() {
92+
while (available.isEmpty() && inUse.size() >= maxSize) {
93+
try {
94+
wait();
95+
} catch (InterruptedException e) {
96+
Thread.currentThread().interrupt();
97+
break;
98+
}
99+
}
67100
if (available.isEmpty()) {
68101
available.add(create());
69102
}
@@ -76,6 +109,7 @@ public synchronized T checkOut() {
76109
public synchronized void checkIn(T instance) {
77110
inUse.remove(instance);
78111
available.add(instance);
112+
notifyAll();
79113
}
80114

81115
@Override
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2026
5+
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
6+
*
7+
* SPDX-License-Identifier: LGPL-3.0-or-later
8+
*
9+
* BSL Language Server is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation; either
12+
* version 3.0 of the License, or (at your option) any later version.
13+
*
14+
* BSL Language Server is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with BSL Language Server.
21+
*/
22+
package com.github._1c_syntax.bsl.languageserver.utils;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.Timeout;
26+
27+
import java.util.concurrent.CountDownLatch;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.atomic.AtomicInteger;
30+
import java.util.concurrent.atomic.AtomicReference;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
34+
import static org.awaitility.Awaitility.await;
35+
36+
class AbstractObjectPoolTest {
37+
38+
private static final class CountingPool extends AbstractObjectPool<Object> {
39+
40+
private final AtomicInteger created = new AtomicInteger();
41+
42+
private CountingPool() {
43+
}
44+
45+
private CountingPool(int maxSize) {
46+
super(maxSize);
47+
}
48+
49+
@Override
50+
protected Object create() {
51+
created.incrementAndGet();
52+
return new Object();
53+
}
54+
}
55+
56+
@Test
57+
void checkOutReusesReturnedInstance() {
58+
var pool = new CountingPool();
59+
60+
var first = pool.checkOut();
61+
pool.checkIn(first);
62+
var second = pool.checkOut();
63+
64+
assertThat(second).isSameAs(first);
65+
assertThat(pool.created.get()).isEqualTo(1);
66+
}
67+
68+
@Test
69+
void unboundedPoolCreatesInstancePerConcurrentCheckout() {
70+
var pool = new CountingPool();
71+
72+
var first = pool.checkOut();
73+
var second = pool.checkOut();
74+
75+
assertThat(second).isNotSameAs(first);
76+
assertThat(pool.created.get()).isEqualTo(2);
77+
}
78+
79+
@Test
80+
@Timeout(10)
81+
void boundedPoolBlocksUntilCheckInAndHandsOverInstance() throws InterruptedException {
82+
var pool = new CountingPool(1);
83+
var first = pool.checkOut();
84+
85+
var handedOver = new AtomicReference<>();
86+
var waiterStarted = new CountDownLatch(1);
87+
var waiterFinished = new CountDownLatch(1);
88+
var waiter = new Thread(() -> {
89+
waiterStarted.countDown();
90+
handedOver.set(pool.checkOut());
91+
waiterFinished.countDown();
92+
});
93+
waiter.start();
94+
95+
assertThat(waiterStarted.await(5, TimeUnit.SECONDS)).isTrue();
96+
// Ожидающий поток не должен создать новый экземпляр сверх лимита
97+
assertThat(waiterFinished.await(200, TimeUnit.MILLISECONDS)).isFalse();
98+
assertThat(pool.created.get()).isEqualTo(1);
99+
100+
pool.checkIn(first);
101+
102+
assertThat(waiterFinished.await(5, TimeUnit.SECONDS)).isTrue();
103+
assertThat(handedOver.get()).isSameAs(first);
104+
assertThat(pool.created.get()).isEqualTo(1);
105+
}
106+
107+
@Test
108+
@Timeout(10)
109+
void interruptedWaiterGetsInstanceOverCapAndKeepsInterruptFlag() throws InterruptedException {
110+
var pool = new CountingPool(1);
111+
pool.checkOut();
112+
113+
var wasInterrupted = new AtomicReference<Boolean>();
114+
var received = new AtomicReference<>();
115+
var waiter = new Thread(() -> {
116+
received.set(pool.checkOut());
117+
wasInterrupted.set(Thread.currentThread().isInterrupted());
118+
});
119+
waiter.start();
120+
121+
// Дождаться входа в ожидание и прервать
122+
await().atMost(5, TimeUnit.SECONDS).until(() -> waiter.getState() == Thread.State.WAITING);
123+
waiter.interrupt();
124+
waiter.join(TimeUnit.SECONDS.toMillis(5));
125+
126+
assertThat(waiter.isAlive()).isFalse();
127+
assertThat(received.get()).isNotNull();
128+
assertThat(pool.created.get()).isEqualTo(2);
129+
assertThat(wasInterrupted.get()).isTrue();
130+
}
131+
132+
@Test
133+
void nonPositiveMaxSizeIsRejected() {
134+
assertThatThrownBy(() -> new CountingPool(0))
135+
.isInstanceOf(IllegalArgumentException.class);
136+
}
137+
138+
@Test
139+
void toStringReportsPoolState() {
140+
var pool = new CountingPool();
141+
var instance = pool.checkOut();
142+
143+
assertThat(pool).hasToString("Pool available=0 inUse=1");
144+
145+
pool.checkIn(instance);
146+
147+
assertThat(pool).hasToString("Pool available=1 inUse=0");
148+
}
149+
}

0 commit comments

Comments
 (0)