Skip to content

Commit 439aa9c

Browse files
authored
Merge branch 'master' into opt-doc
2 parents dd31a43 + 3ef13f8 commit 439aa9c

File tree

15 files changed

+502
-15
lines changed

15 files changed

+502
-15
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Apollo 2.2.0
2020
* [Misc dependency updates](https://github.com/apolloconfig/apollo/pull/4784)
2121
* [Fix the problem that the deletion failure of the system rights management page does not prompt](https://github.com/apolloconfig/apollo/pull/4803)
2222
* [Fix the issue of the system permission management page retrieving non-existent users](https://github.com/apolloconfig/apollo/pull/4802)
23+
* [Add release history cleaning function](https://github.com/apolloconfig/apollo/pull/4813)
2324
* [[Multi-Database Support][pg] Make JdbcUserDetailsManager compat with postgre](https://github.com/apolloconfig/apollo/pull/4790)
2425

2526
------------------

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.concurrent.TimeUnit;
31+
import java.util.stream.Collectors;
3132
import org.springframework.stereotype.Component;
3233

3334
@Component
@@ -46,12 +47,16 @@ public class BizConfig extends RefreshableConfig {
4647
private static final int DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH = 100;
4748
private static final int DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH_INTERVAL_IN_MILLI = 100;//100ms
4849
private static final int DEFAULT_LONG_POLLING_TIMEOUT = 60; //60s
50+
public static final int DEFAULT_RELEASE_HISTORY_RETENTION_SIZE = -1;
4951

5052
private static final Gson GSON = new Gson();
5153

5254
private static final Type namespaceValueLengthOverrideTypeReference =
5355
new TypeToken<Map<Long, Integer>>() {
5456
}.getType();
57+
private static final Type releaseHistoryRetentionSizeOverrideTypeReference =
58+
new TypeToken<Map<String, Integer>>() {
59+
}.getType();
5560

5661
private final BizDBPropertySource propertySource;
5762

@@ -154,6 +159,24 @@ public int accessKeyAuthTimeDiffTolerance() {
154159
DEFAULT_ACCESS_KEY_AUTH_TIME_DIFF_TOLERANCE);
155160
}
156161

162+
public int releaseHistoryRetentionSize() {
163+
int count = getIntProperty("apollo.release-history.retention.size", DEFAULT_RELEASE_HISTORY_RETENTION_SIZE);
164+
return checkInt(count, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_HISTORY_RETENTION_SIZE);
165+
}
166+
167+
public Map<String, Integer> releaseHistoryRetentionSizeOverride() {
168+
String overrideString = getValue("apollo.release-history.retention.size.override");
169+
Map<String, Integer> releaseHistoryRetentionSizeOverride = Maps.newHashMap();
170+
if (!Strings.isNullOrEmpty(overrideString)) {
171+
releaseHistoryRetentionSizeOverride =
172+
GSON.fromJson(overrideString, releaseHistoryRetentionSizeOverrideTypeReference);
173+
}
174+
return releaseHistoryRetentionSizeOverride.entrySet()
175+
.stream()
176+
.filter(entry -> entry.getValue() >= 1)
177+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
178+
}
179+
157180
public int releaseMessageCacheScanInterval() {
158181
int interval = getIntProperty("apollo.release-message-cache-scan.interval", DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL);
159182
return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL);

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseHistoryRepository.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.ctrip.framework.apollo.biz.repository;
1818

1919
import com.ctrip.framework.apollo.biz.entity.ReleaseHistory;
20-
20+
import java.util.List;
2121
import java.util.Set;
2222
import org.springframework.data.domain.Page;
2323
import org.springframework.data.domain.Pageable;
@@ -42,4 +42,8 @@ Page<ReleaseHistory> findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(Stri
4242
@Query("update ReleaseHistory set IsDeleted = true, DeletedAt = ROUND(UNIX_TIMESTAMP(NOW(4))*1000), DataChange_LastModifiedBy = ?4 where AppId=?1 and ClusterName=?2 and NamespaceName = ?3 and IsDeleted = false")
4343
int batchDelete(String appId, String clusterName, String namespaceName, String operator);
4444

45+
Page<ReleaseHistory> findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(String appId, String clusterName, String namespaceName, String branchName, Pageable pageable);
46+
47+
List<ReleaseHistory> findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc(String appId, String clusterName, String namespaceName, String branchName, long maxId);
48+
4549
}

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseHistoryService.java

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,92 @@
1616
*/
1717
package com.ctrip.framework.apollo.biz.service;
1818

19+
import static com.ctrip.framework.apollo.biz.config.BizConfig.DEFAULT_RELEASE_HISTORY_RETENTION_SIZE;
20+
21+
import com.ctrip.framework.apollo.biz.config.BizConfig;
1922
import com.ctrip.framework.apollo.biz.entity.Audit;
2023
import com.ctrip.framework.apollo.biz.entity.ReleaseHistory;
2124
import com.ctrip.framework.apollo.biz.repository.ReleaseHistoryRepository;
25+
import com.ctrip.framework.apollo.biz.repository.ReleaseRepository;
26+
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
27+
import com.ctrip.framework.apollo.tracer.Tracer;
28+
import com.google.common.collect.Queues;
2229
import com.google.gson.Gson;
30+
import java.util.List;
31+
import java.util.Optional;
32+
import java.util.concurrent.BlockingQueue;
33+
import java.util.concurrent.ExecutorService;
34+
import java.util.concurrent.Executors;
35+
import java.util.concurrent.TimeUnit;
36+
import java.util.concurrent.atomic.AtomicBoolean;
37+
import java.util.stream.Collectors;
38+
import javax.annotation.PostConstruct;
39+
import javax.annotation.PreDestroy;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
2342
import org.springframework.data.domain.Page;
43+
import org.springframework.data.domain.PageRequest;
2444
import org.springframework.data.domain.Pageable;
2545
import org.springframework.stereotype.Service;
46+
import org.springframework.transaction.TransactionStatus;
2647
import org.springframework.transaction.annotation.Transactional;
2748

2849
import java.util.Date;
2950
import java.util.Map;
3051
import java.util.Set;
52+
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
53+
import org.springframework.transaction.support.TransactionTemplate;
3154

3255
/**
3356
* @author Jason Song(song_s@ctrip.com)
3457
*/
3558
@Service
3659
public class ReleaseHistoryService {
60+
private static final Logger logger = LoggerFactory.getLogger(ReleaseHistoryService.class);
3761
private static final Gson GSON = new Gson();
62+
private static final int CLEAN_QUEUE_MAX_SIZE = 100;
63+
private final BlockingQueue<ReleaseHistory> releaseClearQueue = Queues.newLinkedBlockingQueue(CLEAN_QUEUE_MAX_SIZE);
64+
private final ExecutorService cleanExecutorService = Executors.newSingleThreadExecutor(
65+
ApolloThreadFactory.create("ReleaseHistoryService", true));
66+
private final AtomicBoolean cleanStopped = new AtomicBoolean(false);
3867

3968
private final ReleaseHistoryRepository releaseHistoryRepository;
69+
private final ReleaseRepository releaseRepository;
4070
private final AuditService auditService;
71+
private final BizConfig bizConfig;
72+
private final TransactionTemplate transactionManager;
4173

4274
public ReleaseHistoryService(
4375
final ReleaseHistoryRepository releaseHistoryRepository,
44-
final AuditService auditService) {
76+
final ReleaseRepository releaseRepository,
77+
final AuditService auditService,
78+
final BizConfig bizConfig,
79+
final TransactionTemplate transactionManager) {
4580
this.releaseHistoryRepository = releaseHistoryRepository;
81+
this.releaseRepository = releaseRepository;
4682
this.auditService = auditService;
83+
this.bizConfig = bizConfig;
84+
this.transactionManager = transactionManager;
4785
}
4886

87+
@PostConstruct
88+
private void initialize() {
89+
cleanExecutorService.submit(() -> {
90+
while (!cleanStopped.get() && !Thread.currentThread().isInterrupted()) {
91+
try {
92+
ReleaseHistory releaseHistory = releaseClearQueue.poll(1, TimeUnit.SECONDS);
93+
if (releaseHistory != null) {
94+
this.cleanReleaseHistory(releaseHistory);
95+
} else {
96+
TimeUnit.MINUTES.sleep(1);
97+
}
98+
} catch (Throwable ex) {
99+
logger.error("Clean releaseHistory failed", ex);
100+
Tracer.logError(ex);
101+
}
102+
}
103+
});
104+
}
49105

50106
public Page<ReleaseHistory> findReleaseHistoriesByNamespace(String appId, String clusterName,
51107
String namespaceName, Pageable
@@ -92,11 +148,86 @@ public ReleaseHistory createReleaseHistory(String appId, String clusterName, Str
92148
auditService.audit(ReleaseHistory.class.getSimpleName(), releaseHistory.getId(),
93149
Audit.OP.INSERT, releaseHistory.getDataChangeCreatedBy());
94150

151+
int releaseHistoryRetentionLimit = this.getReleaseHistoryRetentionLimit(releaseHistory);
152+
if (releaseHistoryRetentionLimit != DEFAULT_RELEASE_HISTORY_RETENTION_SIZE) {
153+
if (!releaseClearQueue.offer(releaseHistory)) {
154+
logger.warn("releaseClearQueue is full, failed to add task to clean queue, " +
155+
"clean queue max size:{}", CLEAN_QUEUE_MAX_SIZE);
156+
}
157+
}
95158
return releaseHistory;
96159
}
97160

98161
@Transactional
99162
public int batchDelete(String appId, String clusterName, String namespaceName, String operator) {
100163
return releaseHistoryRepository.batchDelete(appId, clusterName, namespaceName, operator);
101164
}
165+
166+
private Optional<Long> releaseHistoryRetentionMaxId(ReleaseHistory releaseHistory, int releaseHistoryRetentionSize) {
167+
Page<ReleaseHistory> releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(
168+
releaseHistory.getAppId(),
169+
releaseHistory.getClusterName(),
170+
releaseHistory.getNamespaceName(),
171+
releaseHistory.getBranchName(),
172+
PageRequest.of(releaseHistoryRetentionSize, 1)
173+
);
174+
if (releaseHistoryPage.isEmpty()) {
175+
return Optional.empty();
176+
}
177+
return Optional.of(
178+
releaseHistoryPage
179+
.getContent()
180+
.get(0)
181+
.getId()
182+
);
183+
}
184+
185+
private void cleanReleaseHistory(ReleaseHistory cleanRelease) {
186+
String appId = cleanRelease.getAppId();
187+
String clusterName = cleanRelease.getClusterName();
188+
String namespaceName = cleanRelease.getNamespaceName();
189+
String branchName = cleanRelease.getBranchName();
190+
191+
int retentionLimit = this.getReleaseHistoryRetentionLimit(cleanRelease);
192+
//Second check, if retentionLimit is default value, do not clean
193+
if (retentionLimit == DEFAULT_RELEASE_HISTORY_RETENTION_SIZE) {
194+
return;
195+
}
196+
197+
Optional<Long> maxId = this.releaseHistoryRetentionMaxId(cleanRelease, retentionLimit);
198+
if (!maxId.isPresent()) {
199+
return;
200+
}
201+
202+
boolean hasMore = true;
203+
while (hasMore && !Thread.currentThread().isInterrupted()) {
204+
List<ReleaseHistory> cleanReleaseHistoryList = releaseHistoryRepository.findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc(
205+
appId, clusterName, namespaceName, branchName, maxId.get());
206+
Set<Long> releaseIds = cleanReleaseHistoryList.stream()
207+
.map(ReleaseHistory::getReleaseId)
208+
.collect(Collectors.toSet());
209+
210+
transactionManager.execute(new TransactionCallbackWithoutResult() {
211+
@Override
212+
protected void doInTransactionWithoutResult(TransactionStatus status) {
213+
releaseHistoryRepository.deleteAll(cleanReleaseHistoryList);
214+
releaseRepository.deleteAllById(releaseIds);
215+
}
216+
});
217+
hasMore = cleanReleaseHistoryList.size() == 100;
218+
}
219+
}
220+
221+
private int getReleaseHistoryRetentionLimit(ReleaseHistory releaseHistory) {
222+
String overrideKey = String.format("%s+%s+%s+%s", releaseHistory.getAppId(),
223+
releaseHistory.getClusterName(), releaseHistory.getNamespaceName(), releaseHistory.getBranchName());
224+
225+
Map<String, Integer> overrideMap = bizConfig.releaseHistoryRetentionSizeOverride();
226+
return overrideMap.getOrDefault(overrideKey, bizConfig.releaseHistoryRetentionSize());
227+
}
228+
229+
@PreDestroy
230+
void stopClean() {
231+
cleanStopped.set(true);
232+
}
102233
}

apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/AbstractIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@
3232
)
3333
public abstract class AbstractIntegrationTest {
3434

35+
protected static final String APP_ID = "kl-app";
36+
protected static final String CLUSTER_NAME = "default";
37+
protected static final String NAMESPACE_NAME = "application";
38+
protected static final String BRANCH_NAME = "default";
39+
3540
}

apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/config/BizConfigTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@ public void testReleaseMessageNotificationBatchWithInvalidNumber() throws Except
6969
assertEquals(defaultBatch, bizConfig.releaseMessageNotificationBatch());
7070
}
7171

72+
@Test
73+
public void testReleaseHistoryRetentionSize() {
74+
int someLimit = 20;
75+
when(environment.getProperty("apollo.release-history.retention.size")).thenReturn(String.valueOf(someLimit));
76+
77+
assertEquals(someLimit, bizConfig.releaseHistoryRetentionSize());
78+
}
79+
80+
@Test
81+
public void testReleaseHistoryRetentionSizeOverride() {
82+
int someOverrideLimit = 10;
83+
String overrideValueString = "{'a+b+c+b':10}";
84+
when(environment.getProperty("apollo.release-history.retention.size.override")).thenReturn(overrideValueString);
85+
int overrideValue = bizConfig.releaseHistoryRetentionSizeOverride().get("a+b+c+b");
86+
assertEquals(someOverrideLimit, overrideValue);
87+
88+
overrideValueString = "{'a+b+c+b':0,'a+b+d+b':2}";
89+
when(environment.getProperty("apollo.release-history.retention.size.override")).thenReturn(overrideValueString);
90+
assertEquals(1, bizConfig.releaseHistoryRetentionSizeOverride().size());
91+
overrideValue = bizConfig.releaseHistoryRetentionSizeOverride().get("a+b+d+b");
92+
assertEquals(2, overrideValue);
93+
94+
overrideValueString = "{}";
95+
when(environment.getProperty("apollo.release-history.retention.size.override")).thenReturn(overrideValueString);
96+
assertEquals(0, bizConfig.releaseHistoryRetentionSizeOverride().size());
97+
}
98+
7299
@Test
73100
public void testReleaseMessageNotificationBatchWithNAN() throws Exception {
74101
String someNAN = "someNAN";
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2023 Apollo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.ctrip.framework.apollo.biz.repository;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertTrue;
21+
22+
import com.ctrip.framework.apollo.biz.AbstractIntegrationTest;
23+
import com.ctrip.framework.apollo.biz.entity.ReleaseHistory;
24+
import java.util.List;
25+
import org.junit.Test;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.data.domain.Page;
28+
import org.springframework.data.domain.PageRequest;
29+
import org.springframework.test.context.jdbc.Sql;
30+
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
31+
32+
/**
33+
* @author kl (http://kailing.pub)
34+
* @since 2023/3/23
35+
*/
36+
public class ReleaseHistoryRepositoryTest extends AbstractIntegrationTest {
37+
38+
@Autowired
39+
private ReleaseHistoryRepository releaseHistoryRepository;
40+
41+
@Test
42+
@Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
43+
@Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
44+
public void testFindReleaseHistoryRetentionMaxId() {
45+
Page<ReleaseHistory> releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(1, 1));
46+
assertEquals(5, releaseHistoryPage.getContent().get(0).getId());
47+
48+
releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(2, 1));
49+
assertEquals(4, releaseHistoryPage.getContent().get(0).getId());
50+
51+
releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(5, 1));
52+
assertEquals(1, releaseHistoryPage.getContent().get(0).getId());
53+
54+
releaseHistoryRepository.deleteAll();
55+
releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(1, 1));
56+
assertTrue(releaseHistoryPage.isEmpty());
57+
}
58+
59+
@Test
60+
@Sql(scripts = "/sql/release-history-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
61+
@Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
62+
public void testFindFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc() {
63+
64+
int releaseHistoryRetentionSize = 2;
65+
Page<ReleaseHistory> releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(releaseHistoryRetentionSize, 1));
66+
long releaseMaxId = releaseHistoryPage.getContent().get(0).getId();
67+
List<ReleaseHistory> releaseHistories = releaseHistoryRepository.findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc(
68+
APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, releaseMaxId);
69+
assertEquals(4, releaseHistories.size());
70+
71+
releaseHistoryRetentionSize = 1;
72+
releaseHistoryPage = releaseHistoryRepository.findByAppIdAndClusterNameAndNamespaceNameAndBranchNameOrderByIdDesc(APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, PageRequest.of(releaseHistoryRetentionSize, 1));
73+
releaseMaxId = releaseHistoryPage.getContent().get(0).getId();
74+
releaseHistories = releaseHistoryRepository.findFirst100ByAppIdAndClusterNameAndNamespaceNameAndBranchNameAndIdLessThanEqualOrderByIdAsc(
75+
APP_ID, CLUSTER_NAME, NAMESPACE_NAME, BRANCH_NAME, releaseMaxId);
76+
assertEquals(5, releaseHistories.size());
77+
}
78+
}

0 commit comments

Comments
 (0)