AppConfiguration.java 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. package com.adx.tencent;
  2. import com.adx.tencent.baidu.AdxClient;
  3. import com.adx.tencent.baidu.AuctionPriceEncoder;
  4. import com.adx.tencent.baidu.ConversionClient;
  5. import com.adx.tencent.config.AppProperties;
  6. import com.adx.tencent.conversionsync.ConversionSyncRunner;
  7. import com.adx.tencent.conversionsync.ConversionSyncService;
  8. import com.adx.tencent.conversionsync.ConversionBackfillJobService;
  9. import com.adx.tencent.conversionsync.ManualTencentCallbackService;
  10. import com.adx.tencent.conversionsync.RetryService;
  11. import com.adx.tencent.httpapi.MediaPlacement;
  12. import com.adx.tencent.honor.client.HonorClient;
  13. import com.adx.tencent.honor.service.HonorColdWorker;
  14. import com.adx.tencent.honor.service.HonorPlacement;
  15. import com.adx.tencent.honor.service.HonorRetryService;
  16. import com.adx.tencent.honor.service.HonorTagEventResolver;
  17. import com.adx.tencent.honor.service.HonorTagEventSyncService;
  18. import com.adx.tencent.honor.store.HonorColdStore;
  19. import com.adx.tencent.honor.store.HonorHotStore;
  20. import com.adx.tencent.kuaishou.client.KuaishouClient;
  21. import com.adx.tencent.kuaishou.service.KuaishouColdWorker;
  22. import com.adx.tencent.kuaishou.service.KuaishouPlacement;
  23. import com.adx.tencent.kuaishou.service.KuaishouRetryService;
  24. import com.adx.tencent.kuaishou.service.KuaishouTagEventResolver;
  25. import com.adx.tencent.kuaishou.service.KuaishouTagEventSyncService;
  26. import com.adx.tencent.kuaishou.store.KuaishouColdStore;
  27. import com.adx.tencent.kuaishou.store.KuaishouHotStore;
  28. import com.adx.tencent.leader.LeaderElection;
  29. import com.adx.tencent.report.AdBidReportStore;
  30. import com.adx.tencent.rta.RtaOrderStore;
  31. import com.adx.tencent.tagsync.AccountTagEventResolver;
  32. import com.adx.tencent.tagsync.AccountTagEventSyncService;
  33. import com.adx.tencent.tagsync.TagEventResolver;
  34. import com.adx.tencent.tagsync.TagEventSyncService;
  35. import com.adx.tencent.tencent.TencentClient;
  36. import com.adx.tencent.vivo.client.VivoClient;
  37. import com.adx.tencent.vivo.service.VivoAuthService;
  38. import com.adx.tencent.vivo.service.VivoAdvertiserSyncService;
  39. import com.adx.tencent.vivo.service.VivoColdWorker;
  40. import com.adx.tencent.vivo.service.VivoPlacement;
  41. import com.adx.tencent.vivo.service.VivoRetryService;
  42. import com.adx.tencent.vivo.service.VivoTagEventResolver;
  43. import com.adx.tencent.vivo.service.VivoTagEventSyncService;
  44. import com.adx.tencent.vivo.store.VivoColdStore;
  45. import com.adx.tencent.vivo.store.VivoHotStore;
  46. import com.adx.tencent.storage.RedisHotStore;
  47. import com.adx.tencent.storage.RedisLock;
  48. import com.adx.tencent.storage.TiDBColdStore;
  49. import com.adx.tencent.worker.ColdWorker;
  50. import com.fasterxml.jackson.databind.ObjectMapper;
  51. import com.zaxxer.hikari.HikariConfig;
  52. import com.zaxxer.hikari.HikariDataSource;
  53. import io.lettuce.core.ClientOptions;
  54. import io.lettuce.core.SocketOptions;
  55. import io.lettuce.core.protocol.ProtocolVersion;
  56. import org.apache.ibatis.session.SqlSessionFactory;
  57. import org.mybatis.spring.SqlSessionFactoryBean;
  58. import org.slf4j.Logger;
  59. import org.slf4j.LoggerFactory;
  60. import org.springframework.beans.factory.annotation.Autowired;
  61. import org.springframework.boot.ApplicationArguments;
  62. import org.springframework.boot.ApplicationRunner;
  63. import org.springframework.context.annotation.Bean;
  64. import org.springframework.context.annotation.Configuration;
  65. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
  66. import org.springframework.data.redis.connection.RedisConnectionFactory;
  67. import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
  68. import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
  69. import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
  70. import org.springframework.data.redis.core.StringRedisTemplate;
  71. import org.springframework.lang.Nullable;
  72. import org.springframework.scheduling.annotation.Scheduled;
  73. import org.springframework.stereotype.Component;
  74. import javax.sql.DataSource;
  75. import java.sql.Connection;
  76. import java.time.Duration;
  77. import java.util.HashMap;
  78. import java.util.List;
  79. import java.util.Map;
  80. import java.util.concurrent.ExecutorService;
  81. import java.util.concurrent.Executors;
  82. import java.util.concurrent.atomic.AtomicBoolean;
  83. /**
  84. * 腾讯-百度回传项目 Bean 组装配置类。
  85. * 负责所有核心 Bean 的创建和后台任务(Worker、Leader 选举、定时同步等)。
  86. */
  87. @Configuration
  88. public class AppConfiguration {
  89. private static final Logger log = LoggerFactory.getLogger(AppConfiguration.class);
  90. @Autowired private AppProperties props;
  91. // ─── Redis ───────────────────────────────────────────────────────────────
  92. @Bean
  93. public RedisConnectionFactory redisConnectionFactory() {
  94. String addr = props.getRedisAddr();
  95. String[] parts = addr.split(":", 2);
  96. String host = parts[0];
  97. int port = parts.length > 1 ? Integer.parseInt(parts[1]) : 6379;
  98. RedisStandaloneConfiguration cfg = new RedisStandaloneConfiguration(host, port);
  99. cfg.setDatabase(props.getRedisDb());
  100. if (props.getRedisUsername() != null && !props.getRedisUsername().isBlank()) {
  101. cfg.setUsername(props.getRedisUsername());
  102. }
  103. if (props.getRedisPassword() != null && !props.getRedisPassword().isBlank()) {
  104. cfg.setPassword(props.getRedisPassword());
  105. }
  106. LettuceClientConfiguration clientCfg = LettuceClientConfiguration.builder()
  107. .clientOptions(ClientOptions.builder()
  108. .protocolVersion(ProtocolVersion.RESP2)
  109. .socketOptions(SocketOptions.builder()
  110. .connectTimeout(props.getRedisConnectTimeout())
  111. .build())
  112. .build())
  113. .commandTimeout(props.getRedisCommandTimeout())
  114. .shutdownTimeout(props.getRedisShutdownTimeout())
  115. .build();
  116. LettuceConnectionFactory factory = new LettuceConnectionFactory(cfg, clientCfg);
  117. factory.setValidateConnection(props.isRedisValidateConnection());
  118. factory.setShareNativeConnection(false);
  119. return factory;
  120. }
  121. @Bean
  122. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
  123. return new StringRedisTemplate(factory);
  124. }
  125. @Bean
  126. public RedisHotStore redisHotStore(StringRedisTemplate redis,
  127. ObjectMapper objectMapper,
  128. @Nullable TiDBColdStore coldStore) {
  129. return new RedisHotStore(redis, objectMapper,
  130. coldStore,
  131. "adx:tencent:",
  132. props.getRedisStream(),
  133. props.getBidTtl(),
  134. props.getRedisStreamMaxLen());
  135. }
  136. @Bean
  137. public RedisLock redisLock(StringRedisTemplate redis) {
  138. return new RedisLock(redis);
  139. }
  140. @Bean
  141. public LeaderElection leaderElection(RedisLock redisLock) {
  142. return new LeaderElection(redisLock);
  143. }
  144. // ─── TiDB(可选)────────────────────────────────────────────────────────
  145. @Bean
  146. public DataSource tidbDataSource() {
  147. String url = props.getTidbUrl();
  148. if (url == null || url.isBlank()) {
  149. log.info("TiDB URL is empty; cold storage disabled");
  150. return null;
  151. }
  152. try {
  153. return createDataSource("adx-tidb", url, props.getTidbUsername(), props.getTidbPassword());
  154. } catch (Exception e) {
  155. log.warn("Failed to open TiDB datasource: {}", e.getMessage(), e);
  156. return null;
  157. }
  158. }
  159. @Bean
  160. public TiDBColdStore tiDBColdStore(@Nullable DataSource tidbDataSource, ObjectMapper objectMapper) {
  161. if (tidbDataSource == null) {
  162. log.info("TiDB datasource is empty; cold storage disabled");
  163. return null;
  164. }
  165. try {
  166. SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
  167. TiDBColdStore store = new TiDBColdStore(sqlSessionFactory, objectMapper);
  168. store.migrate();
  169. log.info("TiDB cold store initialized and migrated");
  170. return store;
  171. } catch (Exception e) {
  172. log.warn("Failed to open TiDB: {}; cold storage disabled", e.getMessage(), e);
  173. return null;
  174. }
  175. }
  176. @Bean
  177. public RtaOrderStore rtaOrderStore(@Nullable DataSource tidbDataSource) {
  178. if (tidbDataSource == null) {
  179. return null;
  180. }
  181. try {
  182. SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
  183. RtaOrderStore store = new RtaOrderStore(sqlSessionFactory);
  184. store.migrate();
  185. return store;
  186. } catch (Exception e) {
  187. log.warn("Failed to open RTA order store: {}", e.getMessage(), e);
  188. return null;
  189. }
  190. }
  191. private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception {
  192. SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
  193. factoryBean.setDataSource(dataSource);
  194. factoryBean.setMapperLocations(
  195. new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
  196. return factoryBean.getObject();
  197. }
  198. private DataSource createDataSource(String poolName, String jdbcUrl, String username, String password) {
  199. HikariConfig config = new HikariConfig();
  200. config.setDriverClassName("com.mysql.cj.jdbc.Driver");
  201. config.setJdbcUrl(jdbcUrl);
  202. if (username != null && !username.isBlank()) config.setUsername(username);
  203. if (password != null && !password.isBlank()) config.setPassword(password);
  204. config.setPoolName(poolName);
  205. config.setMinimumIdle(props.getTidbMinimumIdle());
  206. config.setMaximumPoolSize(props.getTidbMaximumPoolSize());
  207. config.setConnectionTimeout(props.getTidbConnectionTimeout().toMillis());
  208. config.setValidationTimeout(props.getTidbValidationTimeout().toMillis());
  209. config.setIdleTimeout(props.getTidbIdleTimeout().toMillis());
  210. config.setMaxLifetime(props.getTidbMaxLifetime().toMillis());
  211. config.setKeepaliveTime(props.getTidbKeepaliveTime().toMillis());
  212. return new HikariDataSource(config);
  213. }
  214. // ─── Baidu 客户端 ────────────────────────────────────────────────────────
  215. @Bean
  216. public AuctionPriceEncoder auctionPriceEncoder() {
  217. return AuctionPriceEncoder.create(
  218. props.getBaiduAuctionPriceEncryption(),
  219. props.getBaiduAuctionPriceEKey(),
  220. props.getBaiduAuctionPriceIKey());
  221. }
  222. @Bean
  223. public AdxClient adxClient(AuctionPriceEncoder encoder, ObjectMapper objectMapper) {
  224. return new AdxClient(
  225. props.getBaiduAdxEndpoint(),
  226. encoder,
  227. objectMapper,
  228. props.getBaiduTrackingMaxRedirects(),
  229. props.getBaiduTrackingMax500Retries());
  230. }
  231. @Bean
  232. public ConversionClient conversionClient(ObjectMapper objectMapper) {
  233. return new ConversionClient(
  234. props.getBaiduConversionBaseUrl(),
  235. props.getBaiduCustomerName(),
  236. props.getBaiduSecret(),
  237. objectMapper);
  238. }
  239. @Bean
  240. public HonorColdStore honorColdStore(@Nullable DataSource tidbDataSource, ObjectMapper objectMapper) {
  241. if (tidbDataSource == null) return null;
  242. try {
  243. SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
  244. HonorColdStore store = new HonorColdStore(sqlSessionFactory, objectMapper);
  245. store.migrate();
  246. return store;
  247. } catch (Exception e) {
  248. log.warn("Failed to open Honor TiDB: {}", e.getMessage(), e);
  249. return null;
  250. }
  251. }
  252. @Bean
  253. public HonorHotStore honorHotStore(StringRedisTemplate redis,
  254. ObjectMapper objectMapper,
  255. @Nullable HonorColdStore honorColdStore) {
  256. if (honorColdStore == null) return null;
  257. return new HonorHotStore(redis, objectMapper, honorColdStore,
  258. "adx:honor:", props.getHonorRedisStream(), props.getBidTtl(), props.getRedisStreamMaxLen());
  259. }
  260. @Bean
  261. public HonorPlacement honorPlacement() {
  262. HonorPlacement p = new HonorPlacement();
  263. p.setBaiduMediaId(props.getBaiduMediaId());
  264. p.setBaiduAppId(props.getBaiduAppId());
  265. p.setBaiduTagId(props.getBaiduTagId());
  266. p.setBidFloor(props.getBaiduBidFloor());
  267. p.setActionTypes(List.of(0, 1, 2));
  268. Map<String, HonorPlacement.HonorAppPlacement> platforms = new HashMap<>();
  269. String honorAndroidAppId = props.getBaiduAndroidAppId();
  270. List<String> honorAndroidTagIds = props.getBaiduAndroidTagIds();
  271. if (honorAndroidAppId != null && !honorAndroidAppId.isBlank()) {
  272. HonorPlacement.HonorAppPlacement android = new HonorPlacement.HonorAppPlacement();
  273. android.setAppId(honorAndroidAppId);
  274. android.setTagIds(honorAndroidTagIds);
  275. platforms.put("android", android);
  276. }
  277. String honorIosAppId = props.getBaiduIosAppId();
  278. List<String> honorIosTagIds = props.getBaiduIosTagIds();
  279. if (honorIosAppId != null && !honorIosAppId.isBlank()) {
  280. HonorPlacement.HonorAppPlacement ios = new HonorPlacement.HonorAppPlacement();
  281. ios.setAppId(honorIosAppId);
  282. ios.setTagIds(honorIosTagIds);
  283. platforms.put("ios", ios);
  284. }
  285. if (!platforms.isEmpty()) p.setPlatforms(platforms);
  286. return p;
  287. }
  288. @Bean
  289. public HonorClient honorClient(ObjectMapper objectMapper) {
  290. return new HonorClient(props.getHonorConversionBaseUrl(), props.getHonorChannelId(), objectMapper);
  291. }
  292. @Bean
  293. public HonorRetryService honorRetryService(@Nullable HonorColdStore honorColdStore, HonorClient honorClient) {
  294. if (honorColdStore == null) return null;
  295. return new HonorRetryService(honorColdStore, honorClient, props.getHonorCallbackRetryLimit());
  296. }
  297. @Bean
  298. public HonorColdWorker honorColdWorker(@Nullable HonorHotStore honorHotStore,
  299. @Nullable HonorColdStore honorColdStore,
  300. ObjectMapper objectMapper) {
  301. if (honorHotStore == null || honorColdStore == null) return null;
  302. return new HonorColdWorker(honorHotStore, honorColdStore,
  303. "honor-cold-writers", props.getWorkerConsumer() + "-honor", props.getWorkerBatch(),
  304. null, props.getRedisStreamMaxLen(), objectMapper);
  305. }
  306. @Bean
  307. public KuaishouColdStore kuaishouColdStore(@Nullable DataSource tidbDataSource, ObjectMapper objectMapper) {
  308. if (tidbDataSource == null) return null;
  309. try {
  310. SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
  311. KuaishouColdStore store = new KuaishouColdStore(sqlSessionFactory, objectMapper);
  312. store.migrate();
  313. return store;
  314. } catch (Exception e) {
  315. log.warn("Failed to open Kuaishou TiDB: {}", e.getMessage(), e);
  316. return null;
  317. }
  318. }
  319. @Bean
  320. public KuaishouHotStore kuaishouHotStore(StringRedisTemplate redis,
  321. ObjectMapper objectMapper,
  322. @Nullable KuaishouColdStore kuaishouColdStore) {
  323. if (kuaishouColdStore == null) return null;
  324. return new KuaishouHotStore(redis, objectMapper, kuaishouColdStore,
  325. "adx:kuaishou:", props.getKuaishouRedisStream(), props.getBidTtl(), props.getRedisStreamMaxLen());
  326. }
  327. @Bean
  328. public KuaishouPlacement kuaishouPlacement() {
  329. KuaishouPlacement p = new KuaishouPlacement();
  330. p.setBaiduMediaId(props.getBaiduMediaId());
  331. p.setBaiduAppId(props.getBaiduAppId());
  332. p.setBaiduTagId(props.getBaiduTagId());
  333. p.setBidFloor(props.getBaiduBidFloor());
  334. p.setActionTypes(List.of(0, 1, 2));
  335. Map<String, KuaishouPlacement.KuaishouAppPlacement> platforms = new HashMap<>();
  336. if (props.getBaiduAndroidAppId() != null && !props.getBaiduAndroidAppId().isBlank()) {
  337. KuaishouPlacement.KuaishouAppPlacement android = new KuaishouPlacement.KuaishouAppPlacement();
  338. android.setAppId(props.getBaiduAndroidAppId());
  339. android.setTagIds(props.getBaiduAndroidTagIds());
  340. platforms.put("android", android);
  341. }
  342. if (props.getBaiduIosAppId() != null && !props.getBaiduIosAppId().isBlank()) {
  343. KuaishouPlacement.KuaishouAppPlacement ios = new KuaishouPlacement.KuaishouAppPlacement();
  344. ios.setAppId(props.getBaiduIosAppId());
  345. ios.setTagIds(props.getBaiduIosTagIds());
  346. platforms.put("ios", ios);
  347. }
  348. if (!platforms.isEmpty()) p.setPlatforms(platforms);
  349. return p;
  350. }
  351. @Bean
  352. public KuaishouClient kuaishouClient(ObjectMapper objectMapper) {
  353. return new KuaishouClient(props.getKuaishouConversionBaseUrl(), objectMapper);
  354. }
  355. @Bean
  356. public KuaishouRetryService kuaishouRetryService(@Nullable KuaishouColdStore kuaishouColdStore,
  357. KuaishouClient kuaishouClient) {
  358. if (kuaishouColdStore == null) return null;
  359. return new KuaishouRetryService(kuaishouColdStore, kuaishouClient, props.getKuaishouCallbackRetryLimit());
  360. }
  361. @Bean
  362. public KuaishouColdWorker kuaishouColdWorker(@Nullable KuaishouHotStore kuaishouHotStore,
  363. @Nullable KuaishouColdStore kuaishouColdStore,
  364. ObjectMapper objectMapper) {
  365. if (kuaishouHotStore == null || kuaishouColdStore == null) return null;
  366. return new KuaishouColdWorker(kuaishouHotStore, kuaishouColdStore,
  367. "kuaishou-cold-writers", props.getWorkerConsumer() + "-kuaishou", props.getWorkerBatch(),
  368. null, props.getRedisStreamMaxLen(), objectMapper);
  369. }
  370. @Bean
  371. public VivoColdStore vivoColdStore(@Nullable DataSource tidbDataSource, ObjectMapper objectMapper) {
  372. if (tidbDataSource == null) return null;
  373. try {
  374. SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
  375. VivoColdStore store = new VivoColdStore(sqlSessionFactory, objectMapper);
  376. store.migrate();
  377. return store;
  378. } catch (Exception e) {
  379. log.warn("Failed to open Vivo TiDB: {}", e.getMessage(), e);
  380. return null;
  381. }
  382. }
  383. @Bean
  384. public VivoHotStore vivoHotStore(StringRedisTemplate redis,
  385. ObjectMapper objectMapper,
  386. @Nullable VivoColdStore vivoColdStore) {
  387. if (vivoColdStore == null) return null;
  388. return new VivoHotStore(redis, objectMapper, vivoColdStore,
  389. "adx:vivo:", props.getVivoRedisStream(), props.getBidTtl(), props.getRedisStreamMaxLen());
  390. }
  391. @Bean
  392. public VivoPlacement vivoPlacement() {
  393. VivoPlacement p = new VivoPlacement();
  394. p.setBaiduMediaId(props.getBaiduMediaId());
  395. p.setBaiduAppId(props.getBaiduAppId());
  396. p.setBaiduTagId(props.getBaiduTagId());
  397. p.setBidFloor(props.getBaiduBidFloor());
  398. p.setActionTypes(List.of(0, 1, 2));
  399. Map<String, VivoPlacement.VivoAppPlacement> platforms = new HashMap<>();
  400. if (props.getBaiduAndroidAppId() != null && !props.getBaiduAndroidAppId().isBlank()) {
  401. VivoPlacement.VivoAppPlacement android = new VivoPlacement.VivoAppPlacement();
  402. android.setAppId(props.getBaiduAndroidAppId());
  403. android.setTagIds(props.getBaiduAndroidTagIds());
  404. platforms.put("android", android);
  405. }
  406. if (props.getBaiduIosAppId() != null && !props.getBaiduIosAppId().isBlank()) {
  407. VivoPlacement.VivoAppPlacement ios = new VivoPlacement.VivoAppPlacement();
  408. ios.setAppId(props.getBaiduIosAppId());
  409. ios.setTagIds(props.getBaiduIosTagIds());
  410. platforms.put("ios", ios);
  411. }
  412. if (!platforms.isEmpty()) p.setPlatforms(platforms);
  413. return p;
  414. }
  415. @Bean
  416. public VivoAuthService vivoAuthService(StringRedisTemplate redisTemplate,
  417. @Nullable VivoColdStore vivoColdStore,
  418. ObjectMapper objectMapper) {
  419. if (vivoColdStore == null) return null;
  420. return new VivoAuthService(
  421. props.getVivoConversionBaseUrl(),
  422. props.getVivoRedirectUri(),
  423. props.getVivoTokenRedisPrefix(),
  424. props.getVivoAdvertiserRedisPrefix(),
  425. redisTemplate,
  426. vivoColdStore,
  427. objectMapper
  428. );
  429. }
  430. @Bean
  431. public VivoClient vivoClient(@Nullable VivoAuthService vivoAuthService, ObjectMapper objectMapper) {
  432. if (vivoAuthService == null) return null;
  433. return new VivoClient(props.getVivoConversionBaseUrl(), vivoAuthService, objectMapper);
  434. }
  435. @Bean
  436. public VivoRetryService vivoRetryService(@Nullable VivoColdStore vivoColdStore,
  437. @Nullable VivoClient vivoClient) {
  438. if (vivoColdStore == null || vivoClient == null) return null;
  439. return new VivoRetryService(vivoColdStore, vivoClient, props.getVivoCallbackRetryLimit());
  440. }
  441. @Bean
  442. public VivoAdvertiserSyncService vivoAdvertiserSyncService(@Nullable VivoAuthService vivoAuthService,
  443. @Nullable VivoColdStore vivoColdStore,
  444. ObjectMapper objectMapper) {
  445. if (vivoAuthService == null || vivoColdStore == null) return null;
  446. return new VivoAdvertiserSyncService(props.getVivoConversionBaseUrl(), vivoAuthService, vivoColdStore, objectMapper);
  447. }
  448. @Bean
  449. public VivoColdWorker vivoColdWorker(@Nullable VivoHotStore vivoHotStore,
  450. @Nullable VivoColdStore vivoColdStore,
  451. ObjectMapper objectMapper) {
  452. if (vivoHotStore == null || vivoColdStore == null) return null;
  453. return new VivoColdWorker(vivoHotStore, vivoColdStore,
  454. "vivo-cold-writers", props.getWorkerConsumer() + "-vivo", props.getWorkerBatch(),
  455. null, props.getRedisStreamMaxLen(), objectMapper);
  456. }
  457. // ─── Tencent 客户端 + Sync 服务 ──────────────────────────────────────────
  458. @Bean
  459. public TencentClient tencentClient(ObjectMapper objectMapper, StringRedisTemplate redisTemplate) {
  460. // token 从 Redis 动态获取,支持外部刷新
  461. String tokenRedisKey = props.getTencentAccessTokenRedisKey();
  462. java.util.function.Supplier<String> tokenSupplier;
  463. if (tokenRedisKey != null && !tokenRedisKey.isBlank()) {
  464. tokenSupplier = () -> redisTemplate.opsForValue().get(tokenRedisKey);
  465. } else {
  466. // 兜底:使用配置文件中的静态 token
  467. String staticToken = props.getTencentAccessToken();
  468. tokenSupplier = () -> staticToken;
  469. }
  470. return new TencentClient(
  471. tokenSupplier,
  472. props.getTencentActMap(),
  473. objectMapper);
  474. }
  475. @Bean
  476. public TagEventResolver tagEventResolver(@Nullable TiDBColdStore coldStore,
  477. StringRedisTemplate redisTemplate) {
  478. if (coldStore == null) return null;
  479. return new TagEventResolver(redisTemplate, coldStore, props.getTagEventSyncRedisPrefix());
  480. }
  481. @Bean
  482. public AccountTagEventResolver accountTagEventResolver(@Nullable TiDBColdStore coldStore,
  483. StringRedisTemplate redisTemplate) {
  484. if (coldStore == null) return null;
  485. return new AccountTagEventResolver(redisTemplate, coldStore, props.getAccountTagEventSyncRedisPrefix());
  486. }
  487. @Bean
  488. public HonorTagEventResolver honorTagEventResolver(@Nullable HonorColdStore honorColdStore,
  489. StringRedisTemplate redisTemplate) {
  490. if (honorColdStore == null) return null;
  491. return new HonorTagEventResolver(redisTemplate, honorColdStore, props.getHonorTagEventSyncRedisPrefix());
  492. }
  493. @Bean
  494. public KuaishouTagEventResolver kuaishouTagEventResolver(@Nullable KuaishouColdStore kuaishouColdStore,
  495. StringRedisTemplate redisTemplate) {
  496. if (kuaishouColdStore == null) return null;
  497. return new KuaishouTagEventResolver(redisTemplate, kuaishouColdStore, props.getKuaishouTagEventSyncRedisPrefix());
  498. }
  499. @Bean
  500. public VivoTagEventResolver vivoTagEventResolver(@Nullable VivoColdStore vivoColdStore,
  501. StringRedisTemplate redisTemplate) {
  502. if (vivoColdStore == null) return null;
  503. return new VivoTagEventResolver(redisTemplate, vivoColdStore, props.getVivoTagEventSyncRedisPrefix());
  504. }
  505. @Bean
  506. public ConversionSyncService conversionSyncService(ConversionClient baiduClient,
  507. RedisHotStore hotStore,
  508. TencentClient tencentClient,
  509. @Nullable TiDBColdStore coldStore,
  510. @Nullable TagEventResolver tagEventResolver,
  511. @Nullable AccountTagEventResolver accountTagEventResolver,
  512. @Nullable HonorTagEventResolver honorTagEventResolver,
  513. @Nullable KuaishouTagEventResolver kuaishouTagEventResolver,
  514. @Nullable VivoTagEventResolver vivoTagEventResolver,
  515. @Nullable HonorHotStore honorHotStore,
  516. @Nullable HonorColdStore honorColdStore,
  517. @Nullable KuaishouHotStore kuaishouHotStore,
  518. @Nullable KuaishouColdStore kuaishouColdStore,
  519. @Nullable VivoHotStore vivoHotStore,
  520. @Nullable VivoColdStore vivoColdStore,
  521. HonorClient honorClient,
  522. KuaishouClient kuaishouClient,
  523. @Nullable VivoClient vivoClient) {
  524. if (coldStore == null) return null;
  525. return new ConversionSyncService(baiduClient, hotStore, tencentClient, coldStore, tagEventResolver,
  526. accountTagEventResolver, honorTagEventResolver, kuaishouTagEventResolver, vivoTagEventResolver,
  527. honorHotStore, honorColdStore, kuaishouHotStore, kuaishouColdStore, vivoHotStore, vivoColdStore,
  528. honorClient, kuaishouClient, vivoClient, props);
  529. }
  530. @Bean
  531. public ConversionSyncRunner conversionSyncRunner(@Nullable ConversionSyncService syncer) {
  532. if (syncer == null) return null;
  533. return new ConversionSyncRunner(syncer,
  534. props.getConversionSyncDateOffsetDays(),
  535. props.getConversionSyncPageSize(),
  536. props.getConversionSyncActs());
  537. }
  538. @Bean
  539. public ConversionBackfillJobService conversionBackfillJobService(@Nullable ConversionSyncRunner syncer) {
  540. if (syncer == null) return null;
  541. return new ConversionBackfillJobService(syncer);
  542. }
  543. @Bean
  544. public RetryService retryService(@Nullable TiDBColdStore coldStore, TencentClient tencentClient) {
  545. if (coldStore == null) return null;
  546. return new RetryService(coldStore, tencentClient, props.getCallbackRetryLimit());
  547. }
  548. @Bean
  549. public ManualTencentCallbackService manualTencentCallbackService(@Nullable TiDBColdStore coldStore,
  550. RedisHotStore hotStore,
  551. TencentClient tencentClient) {
  552. if (coldStore == null) return null;
  553. return new ManualTencentCallbackService(coldStore, hotStore, tencentClient);
  554. }
  555. @Bean
  556. public TagEventSyncService tagEventSyncService(@Nullable TiDBColdStore coldStore,
  557. StringRedisTemplate redisTemplate) {
  558. if (coldStore == null) return null;
  559. // TTL 取同步间隔的 3 倍,避免表中已删除的 tag 在 Redis 长期残留
  560. Duration ttl = props.getTagEventSyncInterval().multipliedBy(3);
  561. return new TagEventSyncService(coldStore, redisTemplate,
  562. props.getTagEventSyncRedisPrefix(), ttl);
  563. }
  564. @Bean
  565. public AccountTagEventSyncService accountTagEventSyncService(@Nullable TiDBColdStore coldStore,
  566. StringRedisTemplate redisTemplate) {
  567. if (coldStore == null) return null;
  568. Duration ttl = props.getAccountTagEventSyncInterval().multipliedBy(3);
  569. return new AccountTagEventSyncService(coldStore, redisTemplate,
  570. props.getAccountTagEventSyncRedisPrefix(), ttl);
  571. }
  572. @Bean
  573. public HonorTagEventSyncService honorTagEventSyncService(@Nullable HonorColdStore honorColdStore,
  574. StringRedisTemplate redisTemplate) {
  575. if (honorColdStore == null) return null;
  576. Duration ttl = props.getHonorTagEventSyncInterval().multipliedBy(3);
  577. return new HonorTagEventSyncService(honorColdStore, redisTemplate,
  578. props.getHonorTagEventSyncRedisPrefix(), ttl);
  579. }
  580. @Bean
  581. public KuaishouTagEventSyncService kuaishouTagEventSyncService(@Nullable KuaishouColdStore kuaishouColdStore,
  582. StringRedisTemplate redisTemplate) {
  583. if (kuaishouColdStore == null) return null;
  584. Duration ttl = props.getKuaishouTagEventSyncInterval().multipliedBy(3);
  585. return new KuaishouTagEventSyncService(kuaishouColdStore, redisTemplate,
  586. props.getKuaishouTagEventSyncRedisPrefix(), ttl);
  587. }
  588. @Bean
  589. public VivoTagEventSyncService vivoTagEventSyncService(@Nullable VivoColdStore vivoColdStore,
  590. StringRedisTemplate redisTemplate) {
  591. if (vivoColdStore == null) return null;
  592. Duration ttl = props.getVivoTagEventSyncInterval().multipliedBy(3);
  593. return new VivoTagEventSyncService(vivoColdStore, redisTemplate,
  594. props.getVivoTagEventSyncRedisPrefix(), ttl);
  595. }
  596. @Bean
  597. public AdBidReportStore adBidReportStore(@Nullable DataSource tidbDataSource) {
  598. if (tidbDataSource == null) return null;
  599. String mainSchema = resolveDatabaseName(tidbDataSource, props.getTidbUrl());
  600. if (mainSchema == null || mainSchema.isBlank()) return null;
  601. AdBidReportStore store = new AdBidReportStore(
  602. tidbDataSource,
  603. mainSchema,
  604. props.getReportDatabaseName(),
  605. props.getAdBidReportRetentionDays(),
  606. props.getTrackingReportRetentionDays(),
  607. props.getAdBidReportArchiveBatchSize(),
  608. props.getAdBidReportArchiveMaxBatchesPerRun(),
  609. props.getAdBidReportStatsLookbackDays());
  610. store.migrate();
  611. return store;
  612. }
  613. // ─── ColdWorker ─────────────────────────────────────────────────────────
  614. @Bean
  615. public ColdWorker coldWorker(RedisHotStore hotStore, @Nullable TiDBColdStore coldStore,
  616. ObjectMapper objectMapper) {
  617. if (coldStore == null) return null;
  618. return new ColdWorker(hotStore, coldStore,
  619. props.getWorkerGroup(), props.getWorkerConsumer(),
  620. props.getWorkerBatch(), null,
  621. props.getRedisStreamMaxLen(), objectMapper);
  622. }
  623. // ─── MediaPlacement(腾讯)───────────────────────────────────────────────
  624. @Bean
  625. public MediaPlacement tencentPlacement() {
  626. MediaPlacement p = new MediaPlacement();
  627. p.setBaiduMediaId(props.getBaiduMediaId());
  628. p.setBaiduAppId(props.getBaiduAppId());
  629. p.setBaiduTagId(props.getBaiduTagId());
  630. p.setBidFloor(props.getBaiduBidFloor());
  631. p.setActionTypes(List.of(0, 1, 2));
  632. Map<String, MediaPlacement.BaiduAppPlacement> platforms = new HashMap<>();
  633. if (props.getBaiduAndroidAppId() != null && !props.getBaiduAndroidAppId().isBlank()) {
  634. MediaPlacement.BaiduAppPlacement android = new MediaPlacement.BaiduAppPlacement();
  635. android.setAppId(props.getBaiduAndroidAppId());
  636. android.setTagIds(props.getBaiduAndroidTagIds());
  637. platforms.put("android", android);
  638. }
  639. if (props.getBaiduIosAppId() != null && !props.getBaiduIosAppId().isBlank()) {
  640. MediaPlacement.BaiduAppPlacement ios = new MediaPlacement.BaiduAppPlacement();
  641. ios.setAppId(props.getBaiduIosAppId());
  642. ios.setTagIds(props.getBaiduIosTagIds());
  643. platforms.put("ios", ios);
  644. }
  645. if (!platforms.isEmpty()) p.setPlatforms(platforms);
  646. return p;
  647. }
  648. private static String resolveDatabaseName(DataSource dataSource, String jdbcUrl) {
  649. try (Connection connection = dataSource.getConnection()) {
  650. String catalog = connection.getCatalog();
  651. if (catalog != null && !catalog.isBlank()) {
  652. return catalog.trim();
  653. }
  654. } catch (Exception ignored) {
  655. }
  656. return extractDatabaseName(jdbcUrl);
  657. }
  658. private static String extractDatabaseName(String jdbcUrl) {
  659. if (jdbcUrl == null || jdbcUrl.isBlank()) {
  660. return null;
  661. }
  662. String value = jdbcUrl.trim();
  663. int scheme = value.indexOf("://");
  664. int searchFrom = scheme >= 0 ? scheme + 3 : 0;
  665. int slash = value.indexOf('/', searchFrom);
  666. if (slash < 0 || slash + 1 >= value.length()) {
  667. return null;
  668. }
  669. int end = value.length();
  670. int question = value.indexOf('?', slash + 1);
  671. if (question >= 0) {
  672. end = Math.min(end, question);
  673. }
  674. int hash = value.indexOf('#', slash + 1);
  675. if (hash >= 0) {
  676. end = Math.min(end, hash);
  677. }
  678. int semicolon = value.indexOf(';', slash + 1);
  679. if (semicolon >= 0) {
  680. end = Math.min(end, semicolon);
  681. }
  682. int nextSlash = value.indexOf('/', slash + 1);
  683. if (nextSlash >= 0 && nextSlash < end) {
  684. end = nextSlash;
  685. }
  686. String db = value.substring(slash + 1, end);
  687. return db == null || db.isBlank() ? null : db.trim();
  688. }
  689. // ─── 后台任务调度 ────────────────────────────────────────────────────────
  690. @Component
  691. static class BackgroundTasks implements ApplicationRunner {
  692. private static final Logger taskLog = LoggerFactory.getLogger(BackgroundTasks.class);
  693. @Autowired private AppProperties props;
  694. @Autowired(required = false) private ColdWorker coldWorker;
  695. @Autowired(required = false) private ConversionSyncRunner conversionSyncRunner;
  696. @Autowired(required = false) private RetryService retryService;
  697. @Autowired(required = false) private TagEventSyncService tagEventSyncService;
  698. @Autowired(required = false) private AccountTagEventSyncService accountTagEventSyncService;
  699. @Autowired(required = false) private LeaderElection leaderElection;
  700. private final AtomicBoolean stopped = new AtomicBoolean(false);
  701. private final ExecutorService executor = Executors.newCachedThreadPool(r -> {
  702. Thread t = new Thread(r);
  703. t.setDaemon(true);
  704. return t;
  705. });
  706. @Override
  707. public void run(ApplicationArguments args) {
  708. // 冷路径 Worker
  709. if (coldWorker != null) {
  710. executor.submit(() -> {
  711. while (!stopped.get()) {
  712. try {
  713. coldWorker.runOnce();
  714. } catch (Exception e) {
  715. taskLog.error("cold worker: {}", e.getMessage(), e);
  716. sleep(1000);
  717. }
  718. }
  719. });
  720. taskLog.info("Cold worker started");
  721. }
  722. // 转化同步
  723. if (conversionSyncRunner != null && props.isConversionSyncEnabled()) {
  724. if (props.isSkipLeaderElection()) {
  725. executor.submit(() -> runConversionSync(() -> stopped.get()));
  726. taskLog.info("Conversion sync started (skip leader election)");
  727. } else if (leaderElection != null) {
  728. executor.submit(() ->
  729. leaderElection.run(
  730. "adx:lock:tencent:conversion-sync",
  731. props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
  732. stopped::get,
  733. (jobStop) -> runConversionSync(jobStop),
  734. e -> taskLog.error("tencent conversion sync leader: {}", e.getMessage(), e)
  735. )
  736. );
  737. taskLog.info("Conversion sync leader election started");
  738. }
  739. } else {
  740. taskLog.warn("Conversion sync NOT started: runner={}, enabled={}",
  741. conversionSyncRunner != null, props.isConversionSyncEnabled());
  742. }
  743. // 回调重试
  744. if (retryService != null && props.isCallbackRetryEnabled()) {
  745. if (props.isSkipLeaderElection()) {
  746. executor.submit(() -> runCallbackRetry(() -> stopped.get()));
  747. taskLog.info("Callback retry started (skip leader election)");
  748. } else if (leaderElection != null) {
  749. executor.submit(() ->
  750. leaderElection.run(
  751. "adx:lock:tencent:callback-retry",
  752. props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
  753. stopped::get,
  754. (jobStop) -> runCallbackRetry(jobStop),
  755. e -> taskLog.error("tencent callback retry leader: {}", e.getMessage(), e)
  756. )
  757. );
  758. taskLog.info("Callback retry leader election started");
  759. }
  760. } else {
  761. taskLog.warn("Callback retry NOT started: retryService={}, enabled={}",
  762. retryService != null, props.isCallbackRetryEnabled());
  763. }
  764. // 广告位回传方式同步
  765. if (tagEventSyncService != null && props.isTagEventSyncEnabled()) {
  766. if (props.isSkipLeaderElection()) {
  767. executor.submit(() -> runTagEventSync(() -> stopped.get()));
  768. taskLog.info("Tag event sync started (skip leader election)");
  769. } else if (leaderElection != null) {
  770. executor.submit(() ->
  771. leaderElection.run(
  772. "adx:lock:tencent:tag-event-sync",
  773. props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
  774. stopped::get,
  775. (jobStop) -> runTagEventSync(jobStop),
  776. e -> taskLog.error("tencent tag event sync leader: {}", e.getMessage(), e)
  777. )
  778. );
  779. taskLog.info("Tag event sync leader election started");
  780. }
  781. } else {
  782. taskLog.warn("Tag event sync NOT started: service={}, enabled={}",
  783. tagEventSyncService != null, props.isTagEventSyncEnabled());
  784. }
  785. // 腾讯账户级广告位回传方式同步
  786. if (accountTagEventSyncService != null && props.isAccountTagEventSyncEnabled()) {
  787. if (props.isSkipLeaderElection()) {
  788. executor.submit(() -> runAccountTagEventSync(() -> stopped.get()));
  789. taskLog.info("Account tag event sync started (skip leader election)");
  790. } else if (leaderElection != null) {
  791. executor.submit(() ->
  792. leaderElection.run(
  793. "adx:lock:tencent:account-tag-event-sync",
  794. props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
  795. stopped::get,
  796. (jobStop) -> runAccountTagEventSync(jobStop),
  797. e -> taskLog.error("tencent account tag event sync leader: {}", e.getMessage(), e)
  798. )
  799. );
  800. taskLog.info("Account tag event sync leader election started");
  801. }
  802. } else {
  803. taskLog.warn("Account tag event sync NOT started: service={}, enabled={}",
  804. accountTagEventSyncService != null, props.isAccountTagEventSyncEnabled());
  805. }
  806. }
  807. private void runConversionSync(LeaderElection.StopSignal jobStop) {
  808. long intervalMs = props.getConversionSyncInterval().toMillis();
  809. taskLog.info("[ConversionSync] task started, interval={}ms", intervalMs);
  810. while (!jobStop.isStopped()) {
  811. try {
  812. taskLog.info("[ConversionSync] executing...");
  813. ConversionSyncService.SyncResult r = conversionSyncRunner.runOnce();
  814. taskLog.info("[ConversionSync] done: fetched={} sent={} failed={}", r.fetched.get(), r.sent.get(), r.failed.get());
  815. } catch (Exception e) {
  816. taskLog.error("[ConversionSync] error: {}", e.getMessage(), e);
  817. }
  818. taskLog.info("[ConversionSync] sleeping {}ms until next run", intervalMs);
  819. sleepResponsive(intervalMs, jobStop);
  820. }
  821. taskLog.info("[ConversionSync] task stopped");
  822. }
  823. @Scheduled(cron = "0 17 1,6,8,10,12,16,20,23 * * ?")
  824. public void runDelayedConversionSync() {
  825. if (conversionSyncRunner == null || !props.isConversionSyncEnabled()) {
  826. return;
  827. }
  828. if (stopped.get()) {
  829. return;
  830. }
  831. Runnable job = () -> {
  832. long startNs = System.nanoTime();
  833. try {
  834. taskLog.info("[ConversionBackfill] executing for offsets=1..7");
  835. ConversionSyncService.SyncResult r = conversionSyncRunner.runBackfillForOffsetsParallel(
  836. List.of(-1, -2, -3, -4, -5, -6, -7), 3);
  837. long costMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
  838. taskLog.info("[ConversionBackfill] done: fetched={} matched={} sent={} failed={} skipped={} alreadySent={}",
  839. r.fetched.get(), r.matched.get(), r.sent.get(), r.failed.get(),
  840. r.skipped.get(), r.alreadySent.get());
  841. taskLog.info("[ConversionBackfill] total cost={}ms", costMs);
  842. } catch (Exception e) {
  843. long costMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
  844. taskLog.error("[ConversionBackfill] error after {}ms: {}", costMs, e.getMessage(), e);
  845. }
  846. };
  847. if (props.isSkipLeaderElection() || leaderElection == null) {
  848. job.run();
  849. return;
  850. }
  851. executor.submit(() ->
  852. leaderElection.runOnce(
  853. "adx:lock:tencent:conversion-backfill",
  854. props.getTaskLockTtl(), props.getTaskLockRenewInterval(),
  855. stopped::get,
  856. stop -> job.run(),
  857. e -> taskLog.error("tencent conversion backfill leader: {}", e.getMessage(), e)
  858. )
  859. );
  860. }
  861. private void runCallbackRetry(LeaderElection.StopSignal jobStop) {
  862. long intervalMs = props.getCallbackRetryInterval().toMillis();
  863. taskLog.info("[CallbackRetry] task started, interval={}ms, limit={}", intervalMs, props.getCallbackRetryLimit());
  864. while (!jobStop.isStopped()) {
  865. try {
  866. taskLog.info("[CallbackRetry] executing...");
  867. RetryService.RetryResult r = retryService.retryTencentCallbacks(props.getCallbackRetryLimit());
  868. taskLog.info("[CallbackRetry] done: fetched={} sent={} failed={}", r.fetched, r.sent, r.failed);
  869. } catch (Exception e) {
  870. taskLog.error("[CallbackRetry] error: {}", e.getMessage(), e);
  871. }
  872. taskLog.info("[CallbackRetry] sleeping {}ms until next run", intervalMs);
  873. sleepResponsive(intervalMs, jobStop);
  874. }
  875. taskLog.info("[CallbackRetry] task stopped");
  876. }
  877. private void runTagEventSync(LeaderElection.StopSignal jobStop) {
  878. long intervalMs = props.getTagEventSyncInterval().toMillis();
  879. taskLog.info("[TagEventSync] task started, interval={}ms", intervalMs);
  880. while (!jobStop.isStopped()) {
  881. try {
  882. int n = tagEventSyncService.syncOnce();
  883. taskLog.info("[TagEventSync] done: synced={}", n);
  884. } catch (Exception e) {
  885. taskLog.error("[TagEventSync] error: {}", e.getMessage(), e);
  886. }
  887. sleepResponsive(intervalMs, jobStop);
  888. }
  889. taskLog.info("[TagEventSync] task stopped");
  890. }
  891. private void runAccountTagEventSync(LeaderElection.StopSignal jobStop) {
  892. long intervalMs = props.getAccountTagEventSyncInterval().toMillis();
  893. taskLog.info("[AccountTagEventSync] task started, interval={}ms", intervalMs);
  894. while (!jobStop.isStopped()) {
  895. try {
  896. int n = accountTagEventSyncService.syncOnce();
  897. taskLog.info("[AccountTagEventSync] done: synced={}", n);
  898. } catch (Exception e) {
  899. taskLog.error("[AccountTagEventSync] error: {}", e.getMessage(), e);
  900. }
  901. sleepResponsive(intervalMs, jobStop);
  902. }
  903. taskLog.info("[AccountTagEventSync] task stopped");
  904. }
  905. private static void sleepResponsive(long ms, LeaderElection.StopSignal jobStop) {
  906. long deadline = System.currentTimeMillis() + ms;
  907. while (!jobStop.isStopped()) {
  908. long remaining = deadline - System.currentTimeMillis();
  909. if (remaining <= 0) break;
  910. try {
  911. Thread.sleep(Math.min(remaining, 200));
  912. } catch (InterruptedException e) {
  913. Thread.currentThread().interrupt();
  914. break;
  915. }
  916. }
  917. }
  918. private static void sleep(long ms) {
  919. try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
  920. }
  921. }
  922. }