Skip to content

Commit 2e5a94a

Browse files
Copilotnixel2007
andauthored
Implement persistent disk cache with EhCache for TypoDiagnostic with WordStatus enum, separated cache managers, and tested EhCacheAdapter component (#3550)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Nikita Fedkin <[email protected]>
1 parent 4bd7ee3 commit 2e5a94a

File tree

12 files changed

+754
-16
lines changed

12 files changed

+754
-16
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,5 @@ bsl-language-server_*.zip
8383
*.log
8484
*.hprof
8585
/.idea/material_theme_project_new.xml
86-
__pycache__/
86+
.bsl-ls-cache/
87+
__pycache__/

build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ dependencies {
7575

7676
// кэширование
7777
api("com.github.ben-manes.caffeine", "caffeine", "3.2.0")
78+
api("org.ehcache:ehcache:3.10.8")
7879

7980
// lsp4j core
8081
api("org.eclipse.lsp4j", "org.eclipse.lsp4j", "0.24.0")
@@ -208,6 +209,14 @@ tasks.test {
208209

209210
val jmockitPath = classpath.find { it.name.contains("jmockit") }!!.absolutePath
210211
jvmArgs("-javaagent:${jmockitPath}")
212+
213+
// Cleanup test cache directories after tests complete
214+
doLast {
215+
val tmpDir = File(System.getProperty("java.io.tmpdir"))
216+
tmpDir.listFiles()?.filter { it.name.startsWith("bsl-ls-cache-") }?.forEach { cacheDir ->
217+
cacheDir.deleteRecursively()
218+
}
219+
}
211220
}
212221

213222
tasks.check {

docs/diagnostics/Typo.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,44 @@
66
Проверка орфографических ошибок осуществляется с помощью [LanguageTool](https://languagetool.org/ru/). Проверяемые строки разбиваются по camelCase
77
и проверяются на соответствие во встроенном словаре.
88

9+
## Кэш
10+
Диагностика использует персистентный кэш на диске (EhCache) для хранения информации об уже проверенных словах. Путь к каталогу кэша задаётся свойством `app.cache.path` в конфигурации приложения.
11+
12+
По умолчанию в приложении установлено:
13+
14+
```properties
15+
app.cache.path=.bsl-ls-cache
16+
```
17+
18+
Это означает, что кэш будет создаваться в каталоге `.bsl-ls-cache` относительно рабочей директории приложения (можно переопределить через переменные окружения или аргументы командной строки).
19+
20+
Рекомендации для CI: кэшируйте или переносите этот каталог между сборками, чтобы избежать повторной персистентной инвалидации и ускорить проверки. Ниже — примеры подходов для популярных CI-платформ.
21+
22+
- GitHub Actions
23+
- Используйте `actions/cache` для сохранения каталога (`.bsl-ls-cache` или путь, заданный в `app.cache.path`) между прогоном сборок и тестов.
24+
25+
- GitLab CI
26+
- В `.gitlab-ci.yml` используйте секцию `cache`:
27+
28+
```yaml
29+
cache:
30+
key: "bsl-ls-typo-cache"
31+
paths:
32+
- .bsl-ls-cache/
33+
policy: pull-push
34+
```
35+
36+
- При необходимости задайте уникальный `key` для разных веток/раннеров.
37+
38+
- Jenkins
39+
- В pipeline можно сохранить каталог кэша между сборками несколькими способами:
40+
- Использовать `stash`/`unstash` для передачи данных между этапами в одной сборке.
41+
- Использовать плагин `Workspace Cleanup` и настроить сохранение workspace на агенте (если агенты постоянные) или архивировать артефакт с помощью `archiveArtifacts` и скачивать при следующих сборках.
42+
- Для Jenkins при использовании динамических агентов (например, Kubernetes) рекомендуется сохранять кэш в сетевом хранилище или в объектном хранилище (S3) и восстанавливать его в начале job.
43+
44+
Общие рекомендации:
45+
- Убедитесь, что путь, указанный в `app.cache.path`, доступен процессу сборки и имеет достаточные права.
46+
947
## Источники
1048
<!-- Необходимо указывать ссылки на все источники, из которых почерпнута информация для создания диагностики -->
1149

src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/TypoDiagnostic.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@
2727
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticSeverity;
2828
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticTag;
2929
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticType;
30+
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.CheckedWordsHolder;
3031
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.JLanguageToolPool;
32+
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.WordStatus;
3133
import com.github._1c_syntax.bsl.languageserver.utils.Trees;
3234
import com.github._1c_syntax.bsl.parser.BSLParser;
3335
import com.github._1c_syntax.bsl.parser.BSLParserRuleContext;
3436
import com.github._1c_syntax.utils.CaseInsensitivePattern;
3537
import lombok.AccessLevel;
3638
import lombok.Getter;
39+
import lombok.RequiredArgsConstructor;
3740
import lombok.extern.slf4j.Slf4j;
3841
import org.antlr.v4.runtime.Token;
3942
import org.apache.commons.lang3.StringUtils;
@@ -49,7 +52,6 @@
4952
import java.util.List;
5053
import java.util.Map;
5154
import java.util.Set;
52-
import java.util.concurrent.ConcurrentHashMap;
5355
import java.util.function.Predicate;
5456
import java.util.regex.Pattern;
5557
import java.util.stream.Collectors;
@@ -63,6 +65,7 @@
6365
}
6466
)
6567
@Slf4j
68+
@RequiredArgsConstructor
6669
public class TypoDiagnostic extends AbstractDiagnostic {
6770

6871
@Getter(lazy = true, value = AccessLevel.PRIVATE)
@@ -71,14 +74,6 @@ public class TypoDiagnostic extends AbstractDiagnostic {
7174
"ru", new JLanguageToolPool(Languages.getLanguageForShortCode("ru"))
7275
);
7376

74-
/**
75-
* Карта, хранящая результат проверки слова (ошибка/нет ошибки) в разрезе языков.
76-
*/
77-
private static final Map<String, Map<String, Boolean>> checkedWords = Map.of(
78-
"en", new ConcurrentHashMap<>(),
79-
"ru", new ConcurrentHashMap<>()
80-
);
81-
8277
private static final Pattern SPACES_PATTERN = Pattern.compile("\\s+");
8378
private static final Pattern QUOTE_PATTERN = Pattern.compile("\"");
8479
private static final String FORMAT_STRING_RU = "Л=|ЧЦ=|ЧДЦ=|ЧС=|ЧРД=|ЧРГ=|ЧН=|ЧВН=|ЧГ=|ЧО=|ДФ=|ДЛФ=|ДП=|БЛ=|БИ=";
@@ -100,6 +95,8 @@ public class TypoDiagnostic extends AbstractDiagnostic {
10095
private static final int DEFAULT_MIN_WORD_LENGTH = 3;
10196
private static final String DEFAULT_USER_WORDS_TO_IGNORE = "";
10297

98+
private final CheckedWordsHolder checkedWordsHolder;
99+
103100
@DiagnosticParameter(
104101
type = Integer.class,
105102
defaultValue = "" + DEFAULT_MIN_WORD_LENGTH
@@ -166,12 +163,11 @@ private Map<String, List<Token>> getTokensMap(
166163
protected void check() {
167164

168165
String lang = info.getResourceString("diagnosticLanguage");
169-
Map<String, Boolean> checkedWordsForLang = checkedWords.get(lang);
170166
Map<String, List<Token>> tokensMap = getTokensMap(documentContext);
171167

172168
// build string of unchecked words
173169
Set<String> uncheckedWords = tokensMap.keySet().stream()
174-
.filter(word -> !checkedWordsForLang.containsKey(word))
170+
.filter(word -> checkedWordsHolder.getWordStatus(lang, word) == WordStatus.MISSING)
175171
.collect(Collectors.toSet());
176172

177173
if (uncheckedWords.isEmpty()) {
@@ -201,10 +197,12 @@ protected void check() {
201197
// check words and mark matched as checked
202198
matches.stream()
203199
.map(ruleMatch -> ruleMatch.getSentence().getTokens()[1].getToken())
204-
.forEach(word -> checkedWordsForLang.put(word, true));
200+
.forEach(word -> checkedWordsHolder.markWordAsError(lang, word));
205201

206202
// mark unmatched words without errors as checked
207-
uncheckedWords.forEach(word -> checkedWordsForLang.putIfAbsent(word, false));
203+
uncheckedWords.stream()
204+
.filter(word -> checkedWordsHolder.getWordStatus(lang, word) == WordStatus.MISSING)
205+
.forEach(word -> checkedWordsHolder.markWordAsNoError(lang, word));
208206

209207
fireDiagnosticOnCheckedWordsWithErrors(tokensMap);
210208
}
@@ -213,10 +211,9 @@ private void fireDiagnosticOnCheckedWordsWithErrors(
213211
Map<String, List<Token>> tokensMap
214212
) {
215213
String lang = info.getResourceString("diagnosticLanguage");
216-
Map<String, Boolean> checkedWordsForLang = checkedWords.get(lang);
217214

218215
tokensMap.entrySet().stream()
219-
.filter(entry -> checkedWordsForLang.getOrDefault(entry.getKey(), false))
216+
.filter(entry -> checkedWordsHolder.getWordStatus(lang, entry.getKey()) == WordStatus.HAS_ERROR)
220217
.forEach((Map.Entry<String, List<Token>> entry) -> {
221218
String word = entry.getKey();
222219
List<Token> tokens = entry.getValue();
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2025
5+
* Alexey Sosnoviy <[email protected]>, Nikita Fedkin <[email protected]> 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.diagnostics.typo;
23+
24+
import org.springframework.cache.annotation.CachePut;
25+
import org.springframework.cache.annotation.Cacheable;
26+
import org.springframework.stereotype.Component;
27+
28+
/**
29+
* Компонент для управления постоянным кэшем проверенных слов для диагностики опечаток.
30+
* Использует Spring Cache с EhCache для хранения кэша на диске.
31+
*/
32+
@Component
33+
public class CheckedWordsHolder {
34+
35+
/**
36+
* Получает статус слова из кэша.
37+
*
38+
* @param lang код языка ("en" или "ru")
39+
* @param word слово, статус которого запрашивается
40+
* @return WordStatus, указывающий, есть ли у слова ошибка, отсутствует ли ошибка, или слово отсутствует в кэше
41+
*/
42+
@Cacheable(value = "typoCache", key = "#lang + ':' + #word", cacheManager = "typoCacheManager")
43+
public WordStatus getWordStatus(String lang, String word) {
44+
return WordStatus.MISSING;
45+
}
46+
47+
/**
48+
* Помечает слово как содержащее ошибку в кэше.
49+
*
50+
* @param lang код языка ("en" или "ru")
51+
* @param word слово, которое помечается как содержащее ошибку
52+
* @return сохранённый WordStatus, указывающий на наличие ошибки
53+
*/
54+
@CachePut(value = "typoCache", key = "#lang + ':' + #word", cacheManager = "typoCacheManager")
55+
public WordStatus markWordAsError(String lang, String word) {
56+
return WordStatus.HAS_ERROR;
57+
}
58+
59+
/**
60+
* Помечает слово как не содержащее ошибку в кэше.
61+
*
62+
* @param lang код языка ("en" или "ru")
63+
* @param word слово, которое помечается как не содержащее ошибку
64+
* @return сохранённый WordStatus, указывающий на отсутствие ошибки
65+
*/
66+
@CachePut(value = "typoCache", key = "#lang + ':' + #word", cacheManager = "typoCacheManager")
67+
public WordStatus markWordAsNoError(String lang, String word) {
68+
return WordStatus.NO_ERROR;
69+
}
70+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2025
5+
* Alexey Sosnoviy <[email protected]>, Nikita Fedkin <[email protected]> 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.diagnostics.typo;
23+
24+
/**
25+
* Статус результата проверки орфографии слова.
26+
*/
27+
public enum WordStatus {
28+
/**
29+
* Слово содержит орфографическую ошибку.
30+
*/
31+
HAS_ERROR,
32+
33+
/**
34+
* Слово написано правильно.
35+
*/
36+
NO_ERROR,
37+
38+
/**
39+
* Слово ещё не проверено.
40+
*/
41+
MISSING
42+
}

src/main/java/com/github/_1c_syntax/bsl/languageserver/infrastructure/CacheConfiguration.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,44 @@
2222
package com.github._1c_syntax.bsl.languageserver.infrastructure;
2323

2424
import com.github.benmanes.caffeine.cache.Caffeine;
25+
import com.github._1c_syntax.bsl.languageserver.diagnostics.typo.WordStatus;
26+
import org.ehcache.config.builders.CacheConfigurationBuilder;
27+
import org.ehcache.config.builders.CacheManagerBuilder;
28+
import org.ehcache.config.builders.ResourcePoolsBuilder;
29+
import org.ehcache.config.units.EntryUnit;
30+
import org.ehcache.config.units.MemoryUnit;
31+
import org.springframework.beans.factory.annotation.Value;
2532
import org.springframework.cache.CacheManager;
2633
import org.springframework.cache.annotation.EnableCaching;
2734
import org.springframework.cache.caffeine.CaffeineCacheManager;
35+
import org.springframework.cache.support.SimpleCacheManager;
2836
import org.springframework.context.annotation.Bean;
2937
import org.springframework.context.annotation.Configuration;
38+
import org.springframework.context.annotation.Primary;
39+
40+
import java.nio.file.Path;
41+
import java.util.List;
3042

3143
/**
3244
* Spring-конфигурация кэширования.
45+
* <p>
46+
* Для typoCache используется EhCache с персистентным хранилищем на диске.
47+
* Для остальных кэшей (например, code lens) используется Caffeine с хранением в памяти.
3348
*/
3449
@Configuration
3550
@EnableCaching
3651
public class CacheConfiguration {
52+
53+
private static final String TYPO_CACHE_NAME = "typoCache";
54+
55+
/**
56+
* Основной менеджер кэша, использующий Caffeine для кэширования в памяти.
57+
* <p>
58+
* Помечен как {@code @Primary}, поэтому используется для всех кэшей по умолчанию,
59+
* если не указан явно другой менеджер кэша (например, {@code typoCacheManager} для typoCache).
60+
*/
3761
@Bean
62+
@Primary
3863
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
3964
var caffeineCacheManager = new CaffeineCacheManager();
4065
caffeineCacheManager.setCaffeine(caffeine);
@@ -45,4 +70,48 @@ public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
4570
public Caffeine<Object, Object> caffeineConfig() {
4671
return Caffeine.newBuilder();
4772
}
73+
74+
/**
75+
* Выделенный менеджер EhCache для typoCache с персистентным хранением на диске.
76+
* <p>
77+
* Настроен программно, без использования XML-конфигурации.
78+
* При закрытии Spring-контекста вызывается метод {@code close()} для корректного завершения работы кэша.
79+
*/
80+
@Bean(destroyMethod = "close")
81+
public org.ehcache.CacheManager ehcacheManager(
82+
@Value("${app.cache.path}") String cacheDirPath
83+
) {
84+
var cacheDir = Path.of(cacheDirPath);
85+
86+
// Configure EhCache cache with disk persistence
87+
var cacheConfig = CacheConfigurationBuilder
88+
.newCacheConfigurationBuilder(
89+
String.class,
90+
WordStatus.class,
91+
ResourcePoolsBuilder.newResourcePoolsBuilder()
92+
.heap(125_000, EntryUnit.ENTRIES)
93+
.disk(50, MemoryUnit.MB, true)
94+
)
95+
.build();
96+
97+
// Build native EhCache manager with persistence
98+
return CacheManagerBuilder.newCacheManagerBuilder()
99+
.with(CacheManagerBuilder.persistence(cacheDir.toFile()))
100+
.withCache(TYPO_CACHE_NAME, cacheConfig)
101+
.build(true);
102+
}
103+
104+
@Bean
105+
public CacheManager typoCacheManager(org.ehcache.CacheManager ehcacheManager) {
106+
var nativeCache = ehcacheManager.getCache(TYPO_CACHE_NAME, String.class, WordStatus.class);
107+
108+
// Wrap the native cache with EhCacheAdapter
109+
var simpleCacheManager = new SimpleCacheManager();
110+
simpleCacheManager.setCaches(List.of(
111+
new EhCacheAdapter<>(nativeCache, TYPO_CACHE_NAME)
112+
));
113+
simpleCacheManager.afterPropertiesSet();
114+
115+
return simpleCacheManager;
116+
}
48117
}

0 commit comments

Comments
 (0)