blob: f0c2a35c4384ed311eba3f80763e160eef317997 [file] [log] [blame]
package build.bazel.dashboard.github.teamtable;
import static build.bazel.dashboard.github.teamtable.GithubTeamTable.Cell;
import build.bazel.dashboard.github.GithubUtils;
import build.bazel.dashboard.github.issuequery.GithubIssueQueryExecutor;
import build.bazel.dashboard.github.repo.GithubRepoService;
import build.bazel.dashboard.github.team.GithubTeam;
import build.bazel.dashboard.github.team.GithubTeamService;
import build.bazel.dashboard.github.teamtable.GithubTeamTable.Row;
import build.bazel.dashboard.github.teamtable.GithubTeamTableRepo.GithubTeamTableData;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope.Subtask;
import java.util.stream.Collectors;
import java.util.concurrent.StructuredTaskScope;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class GithubTeamTableService {
private final GithubTeamTableRepo githubTeamTableRepo;
private final GithubRepoService githubRepoService;
private final GithubTeamService githubTeamService;
private final GithubIssueQueryExecutor githubIssueQueryExecutor;
@Builder
@Value
static class GithubTeamTableCacheKey {
String owner;
String repo;
String tableId;
}
private final AsyncCache<GithubTeamTableCacheKey, GithubTeamTable> tableCache =
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.executor(Executors.newVirtualThreadPerTaskExecutor())
.buildAsync();
public GithubTeamTable findOne(String owner, String repo, String tableId) {
try {
return tableCache
.get(
GithubTeamTableCacheKey.builder().owner(owner).repo(repo).tableId(tableId).build(),
(key) -> loadOne(key.getOwner(), key.getRepo(), key.getTableId()))
.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
GithubTeamTable loadOne(String owner, String repo, String tableId) {
return githubRepoService
.findOne(owner, repo)
.flatMap(
githubRepo ->
githubTeamTableRepo
.findOne(githubRepo.getOwner(), githubRepo.getRepo(), tableId)
.map(
table -> {
var noneTeamOwner = "";
if (githubRepo.getActionOwner() != null) {
noneTeamOwner = githubRepo.getActionOwner();
}
var teams = findAllTeams(owner, repo, noneTeamOwner);
return fetchTable(teams, table);
}))
.orElseGet(() -> GithubTeamTable.buildNone(owner, repo, tableId, ""));
}
private GithubTeamTable fetchTable(List<GithubTeam> teams, GithubTeamTableData table) {
var rows = Lists.<Row>newArrayListWithExpectedSize(teams.size());
try (var scope = new StructuredTaskScope<>()) {
var subtasks = ImmutableList.<Subtask<Row>>builderWithExpectedSize(teams.size());
for (var team : teams) {
var subtask = scope.fork(() -> fetchRow(teams, team, table));
subtasks.add(subtask);
}
try {
scope.join();
for (var subtask : subtasks.build()) {
rows.add(subtask.get());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
rows.sort(Comparator.comparing(row -> row.getTeam().getName()));
return GithubTeamTable.builder()
.owner(table.getOwner())
.repo(table.getRepo())
.id(table.getId())
.name(table.getName())
.headers(
table.getHeaders().stream()
.map(
header ->
GithubTeamTable.Header.builder()
.id(header.getId())
.name(header.getName())
.build())
.collect(Collectors.toList()))
.rows(rows)
.build();
}
record CellEntry(String headerId, Cell cell) {}
private Row fetchRow(List<GithubTeam> teams, GithubTeam team, GithubTeamTableData table) {
var cells = new HashMap<String, Cell>();
try (var scope = new StructuredTaskScope<>()) {
var subtasks =
ImmutableList.<Subtask<CellEntry>>builderWithExpectedSize(table.getHeaders().size());
for (var header : table.getHeaders()) {
String query = interceptQuery(teams, team, header.getQuery());
var subtask =
scope.fork(
() -> {
var count =
githubIssueQueryExecutor.fetchQueryResultCount(
team.getOwner(), team.getRepo(), query);
return new CellEntry(
header.getId(),
Cell.builder()
.url(
GithubUtils.buildIssueQueryUrl(
team.getOwner(), team.getRepo(), query))
.count(count)
.build());
});
subtasks.add(subtask);
}
try {
scope.join();
for (var future : subtasks.build()) {
var entry = future.get();
cells.put(entry.headerId, entry.cell);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return Row.builder().team(GithubTeamTable.Team.create(team)).cells(cells).build();
}
private String interceptQuery(List<GithubTeam> teams, GithubTeam team, String query) {
String teamQuery;
if (team.isNone()) {
teamQuery =
teams.stream()
.map(GithubTeam::getLabel)
.filter(label -> !label.equals(""))
.map(label -> "-label:" + label)
.collect(Collectors.joining(" "));
} else {
teamQuery = "label:" + team.getLabel();
}
return String.format("%s %s", query, teamQuery);
}
private ImmutableList<GithubTeam> findAllTeams(String owner, String repo, String noneTeamOwner) {
var teams = githubTeamService.findAll(owner, repo);
return ImmutableList.<GithubTeam>builderWithExpectedSize(teams.size() + 1)
.addAll(teams)
.add(GithubTeam.buildNone(owner, repo, noneTeamOwner))
.build();
}
}