Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,32 @@
package com.github._1c_syntax.bsl.languageserver.diagnostics.typo;

import com.github._1c_syntax.bsl.languageserver.utils.AbstractObjectPool;
import lombok.AllArgsConstructor;
import org.languagetool.JLanguageTool;
import org.languagetool.Language;
import org.languagetool.rules.Rule;

import java.util.function.Predicate;

@AllArgsConstructor
/**
* Пул экземпляров {@link JLanguageTool} для одного языка.
* <p>
* Пул ограничен: каждый экземпляр при создании загружает собственную копию
* полного набора правил языка (десятки мегабайт на инстанс, отключение правил
* память не освобождает), поэтому рост пула до числа потоков анализа
* недопустим по памяти. При исчерпании лимита {@link #checkOut()} ждёт
* возврата экземпляра.
*/
public class JLanguageToolPool extends AbstractObjectPool<JLanguageTool> {

private static final int MAX_POOL_SIZE = 4;

private final Language language;

public JLanguageToolPool(Language language) {
super(Math.min(MAX_POOL_SIZE, Runtime.getRuntime().availableProcessors()));
this.language = language;
}

@Override
protected JLanguageTool create() {
JLanguageTool languageTool = new JLanguageTool(language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,53 @@

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

private final Set<T> available = new HashSet<>();
private final Set<T> inUse = new HashSet<>();
private final int maxSize;

protected AbstractObjectPool() {
this(Integer.MAX_VALUE);
}

/**
* @param maxSize максимальное суммарное количество объектов пула
* (доступных и выданных); не меньше 1.
*/
protected AbstractObjectPool(int maxSize) {
if (maxSize < 1) {
throw new IllegalArgumentException("maxSize must be >= 1, got: " + maxSize);
}
this.maxSize = maxSize;
}

protected abstract T create();

/**
* Checkout object from pool.
* <p>
* Если пул пуст и лимит объектов исчерпан — ждёт возврата объекта.
* Прерывание потока во время ожидания не прерывает выдачу: флаг прерывания
* восстанавливается, а прерванный поток получает объект сверх лимита
* (пул может временно превысить {@code maxSize} на число прерванных ожидающих).
*/
public synchronized T checkOut() {
while (available.isEmpty() && inUse.size() >= maxSize) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if (available.isEmpty()) {
available.add(create());
}
Expand All @@ -76,6 +109,7 @@ public synchronized T checkOut() {
public synchronized void checkIn(T instance) {
inUse.remove(instance);
available.add(instance);
notifyAll();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зачем?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

До ограничения пула checkOut() никогда не ждал: при пустом available сразу создавался новый экземпляр. С появлением maxSize поток при исчерпании лимита встаёт в wait() — без notify* в checkIn() он не проснётся никогда.

notifyAll вместо notify — по замечанию Sonar (java:S2446): notify будит произвольный поток. Ожидающие здесь однородны, так что технически хватило бы и notify, но notifyAll — стандартный безопасный паттерн, а лишних пробуждений при размере пула ≤4 единицы.

}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2026
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.utils;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;

class AbstractObjectPoolTest {

private static final class CountingPool extends AbstractObjectPool<Object> {

private final AtomicInteger created = new AtomicInteger();

private CountingPool() {
}

private CountingPool(int maxSize) {
super(maxSize);
}

@Override
protected Object create() {
created.incrementAndGet();
return new Object();
}
}

@Test
void checkOutReusesReturnedInstance() {
var pool = new CountingPool();

var first = pool.checkOut();
pool.checkIn(first);
var second = pool.checkOut();

assertThat(second).isSameAs(first);
assertThat(pool.created.get()).isEqualTo(1);
}

@Test
void unboundedPoolCreatesInstancePerConcurrentCheckout() {
var pool = new CountingPool();

var first = pool.checkOut();
var second = pool.checkOut();

assertThat(second).isNotSameAs(first);
assertThat(pool.created.get()).isEqualTo(2);
}

@Test
@Timeout(10)
void boundedPoolBlocksUntilCheckInAndHandsOverInstance() throws InterruptedException {
var pool = new CountingPool(1);
var first = pool.checkOut();

var handedOver = new AtomicReference<>();
var waiterStarted = new CountDownLatch(1);
var waiterFinished = new CountDownLatch(1);
var waiter = new Thread(() -> {
waiterStarted.countDown();
handedOver.set(pool.checkOut());
waiterFinished.countDown();
});
waiter.start();

assertThat(waiterStarted.await(5, TimeUnit.SECONDS)).isTrue();
// Ожидающий поток не должен создать новый экземпляр сверх лимита
assertThat(waiterFinished.await(200, TimeUnit.MILLISECONDS)).isFalse();
assertThat(pool.created.get()).isEqualTo(1);

pool.checkIn(first);

assertThat(waiterFinished.await(5, TimeUnit.SECONDS)).isTrue();
assertThat(handedOver.get()).isSameAs(first);
assertThat(pool.created.get()).isEqualTo(1);
}

@Test
@Timeout(10)
void interruptedWaiterGetsInstanceOverCapAndKeepsInterruptFlag() throws InterruptedException {
var pool = new CountingPool(1);
pool.checkOut();

var wasInterrupted = new AtomicReference<Boolean>();
var received = new AtomicReference<>();
var waiter = new Thread(() -> {
received.set(pool.checkOut());
wasInterrupted.set(Thread.currentThread().isInterrupted());
});
waiter.start();

// Дождаться входа в ожидание и прервать
await().atMost(5, TimeUnit.SECONDS).until(() -> waiter.getState() == Thread.State.WAITING);
waiter.interrupt();
waiter.join(TimeUnit.SECONDS.toMillis(5));

assertThat(waiter.isAlive()).isFalse();
assertThat(received.get()).isNotNull();
assertThat(pool.created.get()).isEqualTo(2);
assertThat(wasInterrupted.get()).isTrue();
}

@Test
void nonPositiveMaxSizeIsRejected() {
assertThatThrownBy(() -> new CountingPool(0))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void toStringReportsPoolState() {
var pool = new CountingPool();
var instance = pool.checkOut();

assertThat(pool).hasToString("Pool available=0 inUse=1");

pool.checkIn(instance);

assertThat(pool).hasToString("Pool available=1 inUse=0");
}
}
Loading