Skip to content

Commit

Permalink
Trigger cache update only for changed projects
Browse files Browse the repository at this point in the history
Closes gh-23
  • Loading branch information
mbhave committed Nov 9, 2024
1 parent aed4f64 commit 4e49abe
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/main/java/io/spring/projectapi/ProjectRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
*/
public interface ProjectRepository {

void update();
void update(List<String> changes);

Collection<Project> getProjects();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class GithubProjectRepository implements ProjectRepository {
}

@Override
public void update() {
this.projectData = ProjectData.load(this.githubQueries);
public void update(List<String> changes) {
this.projectData = ProjectData.update(this.projectData, changes, this.githubQueries);
}

@Override
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/io/spring/projectapi/github/GithubQueries.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
Expand All @@ -33,7 +35,9 @@
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

/**
Expand All @@ -59,6 +63,8 @@ public class GithubQueries {

private static final String DEFAULT_SUPPORT_POLICY = "SPRING_BOOT";

private static final Pattern PROJECT_FILE = Pattern.compile("project\\/(.*)\\/.*");

private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};

Expand Down Expand Up @@ -96,6 +102,95 @@ ProjectData getData() {
return new ProjectData(projects, documentation, support, supportPolicy);
}

ProjectData updateData(ProjectData data, List<String> changes) {
Assert.notNull(data, "Project data should not be null");
Map<String, Project> projects = new LinkedHashMap<>(data.project());
Map<String, List<ProjectDocumentation>> documentation = new LinkedHashMap<>(data.documentation());
Map<String, List<ProjectSupport>> support = new LinkedHashMap<>(data.support());
Map<String, String> supportPolicy = new LinkedHashMap<>(data.supportPolicy());
Map<String, Boolean> checkedProjects = new LinkedHashMap<>();
try {
changes.forEach((change) -> {
ProjectFile file = ProjectFile.from(change);
if (ProjectFile.OTHER.equals(file)) {
return;
}
updateData(change, file, projects, supportPolicy, documentation, support, checkedProjects);
});
}
catch (Exception ex) {
logger.debug("Could not update data due to '%s'".formatted(ex.getMessage()));
}
return new ProjectData(projects, documentation, support, supportPolicy);
}

private void updateData(String change, ProjectFile file, Map<String, Project> projects,
Map<String, String> supportPolicy, Map<String, List<ProjectDocumentation>> documentation,
Map<String, List<ProjectSupport>> support, Map<String, Boolean> checkedprojects) {
Matcher matcher = PROJECT_FILE.matcher(change);
if (!matcher.matches()) {
return;
}
String slug = matcher.group(1);
if (checkedprojects.get(slug) == null) {
checkedprojects.put(slug, doesProjectExist(slug));
}
if (checkedprojects.get(slug)) {
updateFromIndex(file, projects, supportPolicy, slug);
updateDocumentation(file, documentation, slug);
updateSupport(file, support, slug);
return;
}
projects.remove(slug);
documentation.remove(slug);
support.remove(slug);
supportPolicy.remove(slug);
}

private void updateSupport(ProjectFile file, Map<String, List<ProjectSupport>> support, String slug) {
if (ProjectFile.SUPPORT.equals(file)) {
List<ProjectSupport> projectSupports = getProjectSupports(slug);
support.put(slug, projectSupports);
}
}

private void updateDocumentation(ProjectFile file, Map<String, List<ProjectDocumentation>> documentation,
String slug) {
if (ProjectFile.DOCUMENTATION.equals(file)) {
List<ProjectDocumentation> projectDocumentation = getProjectDocumentations(slug);
documentation.put(slug, projectDocumentation);
}
}

private void updateFromIndex(ProjectFile file, Map<String, Project> projects, Map<String, String> supportPolicy,
String slug) {
if (ProjectFile.INDEX.equals(file)) {
ResponseEntity<Map<String, Object>> response = getFile(slug, "index.md");
Project project = getProject(response, slug);
if (project != null) {
projects.put(slug, project);
}
String policy = getProjectSupportPolicy(response, slug);
supportPolicy.put(slug, policy);
}
}

private boolean doesProjectExist(String projectSlug) {
RequestEntity<Void> request = RequestEntity.get("/project/{projectSlug}?ref=" + this.branch, projectSlug)
.build();
try {
this.restTemplate.exchange(request, STRING_OBJECT_MAP);
}
catch (Exception ex) {
if (ex instanceof HttpClientErrorException) {
if (((HttpClientErrorException) ex).getStatusCode().value() == 404) {
return false;
}
}
}
return true;
}

private void populateData(Map<String, Object> project, Map<String, Project> projects,
Map<String, List<ProjectDocumentation>> documentation, Map<String, List<ProjectSupport>> support,
Map<String, String> supportPolicy) {
Expand Down Expand Up @@ -188,4 +283,29 @@ private String getFileContent(ResponseEntity<Map<String, Object>> exchange) {
return new String(contents);
}

enum ProjectFile {

INDEX,

SUPPORT,

DOCUMENTATION,

OTHER;

static ProjectFile from(String fileName) {
if (fileName.contains("index.md")) {
return INDEX;
}
if (fileName.contains("documentation.json")) {
return DOCUMENTATION;
}
if (fileName.contains("support.json")) {
return SUPPORT;
}
return OTHER;
}

}

}
20 changes: 16 additions & 4 deletions src/main/java/io/spring/projectapi/github/ProjectData.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.List;
import java.util.Map;

import org.jetbrains.annotations.NotNull;

/**
* Represents cached data from Github.
*
Expand All @@ -34,10 +36,20 @@ record ProjectData(Map<String, Project> project, Map<String, List<ProjectDocumen

public static ProjectData load(GithubQueries githubQueries) {
ProjectData data = githubQueries.getData();
Map<String, Project> projects = data.project();
Map<String, List<ProjectDocumentation>> documentation = data.documentation();
Map<String, List<ProjectSupport>> support = data.support();
Map<String, String> supportPolicy = data.supportPolicy();
return getImmutableProjectData(data);
}

public static ProjectData update(ProjectData data, List<String> changes, GithubQueries githubQueries) {
ProjectData updatedData = githubQueries.updateData(data, changes);
return getImmutableProjectData(updatedData);
}

@NotNull
private static ProjectData getImmutableProjectData(ProjectData updatedData) {
Map<String, Project> projects = updatedData.project();
Map<String, List<ProjectDocumentation>> documentation = updatedData.documentation();
Map<String, List<ProjectSupport>> support = updatedData.support();
Map<String, String> supportPolicy = updatedData.supportPolicy();
return new ProjectData(Map.copyOf(projects), Map.copyOf(documentation), Map.copyOf(support),
Map.copyOf(supportPolicy));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.crypto.Mac;
Expand Down Expand Up @@ -87,6 +89,7 @@ private void verifyHmacSignature(String message, String signature) {
}

@PostMapping("/refresh_cache")
@SuppressWarnings("unchecked")
public ResponseEntity<String> refresh(@RequestBody String payload,
@RequestHeader("X-Hub-Signature") String signature,
@RequestHeader(name = "X-GitHub-Event", required = false, defaultValue = "push") String event)
Expand All @@ -97,10 +100,26 @@ public ResponseEntity<String> refresh(@RequestBody String payload,
}
Map<?, ?> push = this.objectMapper.readValue(payload, Map.class);
logPayload(push);
this.repository.update();
List<Map<String, ?>> commits = (List<Map<String, ?>>) push.get("commits");
List<String> changes = getChangedFiles(commits);
this.repository.update(changes);
return ResponseEntity.ok("{ \"message\": \"Successfully processed cache refresh\" }");
}

@SuppressWarnings("unchecked")
private static List<String> getChangedFiles(List<Map<String, ?>> commits) {
List<String> changedFiles = new ArrayList<>();
commits.forEach((commit) -> {
List<String> added = (List<String>) commit.get("added");
List<String> removed = (List<String>) commit.get("removed");
List<String> modified = (List<String>) commit.get("modified");
changedFiles.addAll(added);
changedFiles.addAll(removed);
changedFiles.addAll(modified);
});
return changedFiles.stream().distinct().toList();
}

@ExceptionHandler(WebhookAuthenticationException.class)
public ResponseEntity<String> handleWebhookAuthenticationFailure(WebhookAuthenticationException exception) {
logger.error("Webhook authentication failure: " + exception.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@
import io.spring.projectapi.github.Project.Status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.verification.VerificationMode;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atMostOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

Expand All @@ -43,27 +41,32 @@ class GithubProjectRepositoryTests {

private GithubQueries githubQueries;

private ProjectData data;

@BeforeEach
void setup() {
this.githubQueries = mock(GithubQueries.class);
setupGithubResponse("spring-boot");
this.data = getData("spring-boot");
given(this.githubQueries.getData()).willReturn(this.data);
this.projectRepository = new GithubProjectRepository(this.githubQueries);
}

@Test
void dataLoadedOnBeanCreation() {
validateCachedValues("spring-boot");
verifyCacheUpdate(atMostOnce());
verify(this.githubQueries).getData();
}

@Test
void updateRefreshesCache() {
setupGithubResponse("spring-boot-updated");
this.projectRepository.update();
List<String> changes = List.of("project/spring-boot-updated/index.md",
"project/spring-boot-updated/documentation.json", "project/spring-boot-updated/support.json");
given(this.githubQueries.updateData(any(), any())).willReturn(getData("spring-boot-updated"));
this.projectRepository.update(changes);
assertThatExceptionOfType(NoSuchGithubProjectException.class)
.isThrownBy(() -> this.projectRepository.getProject("spring-boot"));
validateCachedValues("spring-boot-updated");
verifyCacheUpdate(Mockito.atMost(2));
verify(this.githubQueries).updateData(this.data, changes);
}

@Test
Expand Down Expand Up @@ -133,14 +136,6 @@ private void validateCachedValues(String projectSlug) {
assertThat(policy).isEqualTo("UPSTREAM");
}

private void setupGithubResponse(String project) {
given(this.githubQueries.getData()).willReturn(getData(project));
}

private void verifyCacheUpdate(VerificationMode mode) {
verify(this.githubQueries, mode).getData();
}

private ProjectData getData(String project) {
return new ProjectData(getProjects(project), getProjectDocumentation(project), getProjectSupports(project),
getProjectSupportPolicy(project));
Expand Down
Loading

0 comments on commit 4e49abe

Please sign in to comment.