| package build.bazel.dashboard.github.sync.issue; |
| |
| import static build.bazel.dashboard.utils.RxJavaVirtualThread.completable; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import build.bazel.dashboard.github.issue.GithubIssueService; |
| import build.bazel.dashboard.github.issuecomment.GithubIssueCommentService; |
| import build.bazel.dashboard.github.repo.GithubRepoService; |
| import build.bazel.dashboard.utils.JsonStateStore; |
| import build.bazel.dashboard.utils.JsonStateStore.JsonState; |
| import com.fasterxml.jackson.databind.PropertyNamingStrategies; |
| import com.fasterxml.jackson.databind.annotation.JsonNaming; |
| import io.reactivex.rxjava3.core.Completable; |
| import java.time.Instant; |
| import javax.annotation.Nullable; |
| import lombok.Builder; |
| import lombok.RequiredArgsConstructor; |
| import lombok.Value; |
| import lombok.With; |
| import lombok.extern.slf4j.Slf4j; |
| import org.springframework.scheduling.annotation.Scheduled; |
| import org.springframework.web.bind.annotation.PathVariable; |
| import org.springframework.web.bind.annotation.PutMapping; |
| import org.springframework.web.bind.annotation.RequestParam; |
| import org.springframework.web.bind.annotation.RestController; |
| |
| @RestController |
| @RequiredArgsConstructor |
| @Slf4j |
| public class GithubSyncIssueTask { |
| private final GithubRepoService githubRepoService; |
| private final GithubIssueService githubIssueService; |
| private final GithubIssueCommentService githubIssueCommentService; |
| private final JsonStateStore jsonStateStore; |
| |
| @Builder |
| @Value |
| @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) |
| static class SyncIssueState { |
| String owner; |
| String repo; |
| int start; |
| @With int current; |
| int end; |
| Instant createdAt; |
| } |
| |
| @PutMapping("/internal/github/{owner}/{repo}/sync/issues") |
| public Completable saveNewSyncIssueState( |
| @PathVariable("owner") String owner, |
| @PathVariable("repo") String repo, |
| @RequestParam(name = "start") Integer start, |
| @RequestParam(name = "count") Integer count) { |
| return completable(() -> saveNewSyncIssueState(owner, repo, start, start + count, null)); |
| } |
| |
| @PutMapping("/internal/github/{owner}/{repo}/sync/issues/all") |
| public Completable saveAllSyncIssueState( |
| @PathVariable("owner") String owner, @PathVariable("repo") String repo) { |
| return completable(() -> saveAllSyncIssueState(owner, repo, null)); |
| } |
| |
| private void saveNewSyncIssueState( |
| String owner, String repo, Integer start, Integer end, @Nullable Instant lastTimestamp) { |
| jsonStateStore.save( |
| buildSyncStateKey(owner, repo), |
| lastTimestamp, |
| SyncIssueState.builder() |
| .owner(owner) |
| .repo(repo) |
| .start(start) |
| .current(start) |
| .end(end) |
| .createdAt(Instant.now()) |
| .build()); |
| } |
| |
| private void saveAllSyncIssueState(String owner, String repo, @Nullable Instant lastTimestamp) { |
| var maxIssueNumber = githubIssueService.findMaxIssueNumber(owner, repo); |
| saveNewSyncIssueState(owner, repo, 1, maxIssueNumber + 100, lastTimestamp); |
| } |
| |
| @Scheduled(cron = "0 0 0 * * *", zone = "UTC") |
| public void startNewSyncIfNotExisting() { |
| for (var repo : githubRepoService.findAll()) { |
| String stateKey = buildSyncStateKey(repo.getOwner(), repo.getRepo()); |
| var jsonState = jsonStateStore.load(stateKey, SyncIssueState.class); |
| if (jsonState.getData() == null) { |
| saveAllSyncIssueState(repo.getOwner(), repo.getRepo(), jsonState.getTimestamp()); |
| } |
| } |
| } |
| |
| // We have a rate limit 5000/hour = 1.4/sec. |
| @Scheduled(fixedDelay = 1000) |
| public void syncGithubIssues() throws Throwable { |
| for (var repo : githubRepoService.findAll()) { |
| var jsonState = |
| jsonStateStore.load( |
| buildSyncStateKey(repo.getOwner(), repo.getRepo()), SyncIssueState.class); |
| if (jsonState.getData() != null) { |
| this.syncGithubIssue(jsonState); |
| } |
| } |
| } |
| |
| private void syncGithubIssue(JsonState<SyncIssueState> jsonState) throws Throwable { |
| SyncIssueState data = jsonState.getData(); |
| checkNotNull(data); |
| |
| if (data.current >= data.end) { |
| jsonStateStore.delete(jsonState.getKey(), jsonState.getTimestamp()); |
| return; |
| } |
| |
| var result = |
| githubIssueService.fetchAndSave(data.getOwner(), data.getRepo(), data.getCurrent()); |
| if (result.getError() != null) { |
| throw result.getError(); |
| } |
| |
| githubIssueCommentService.syncIssueComments(data.getOwner(), data.getRepo(), data.getCurrent()); |
| jsonStateStore.save( |
| jsonState.getKey(), jsonState.getTimestamp(), data.withCurrent(data.getCurrent() + 1)); |
| } |
| |
| private String buildSyncStateKey(String owner, String repo) { |
| return String.format("sync-github-issues/%s/%s", owner, repo); |
| } |
| } |