Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
echo "NATS_DURABLE_CONSUMER_NAME=${{ vars.NATS_DURABLE_CONSUMER_NAME }}" >> .env
echo "NATS_CONSUMER_INACTIVE_THRESHOLD_MINUTES=${{ vars.NATS_CONSUMER_INACTIVE_THRESHOLD_MINUTES }}" >> .env
echo "NATS_CONSUMER_ACK_WAIT_SECONDS=${{ vars.NATS_CONSUMER_ACK_WAIT_SECONDS }}" >> .env
echo "NATS_CONSUMER_MAX_ACK_PENDING=${{ vars.NATS_CONSUMER_MAX_ACK_PENDING }}" >> .env
echo "WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }}" >> .env
echo "REPOSITORY_NAME=${{ vars.REPOSITORY_NAME }}" >> .env
echo "ORGANIZATION_NAME=${{ vars.ORGANIZATION_NAME }}" >> .env
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ public WebMvcConfigurer corsConfigurer() {

@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
registry.addWebRequestInterceptor(requestInterceptor);
// Order last (Integer.MAX_VALUE == Ordered.LOWEST_PRECEDENCE) so this runs AFTER Spring's
// Open-Session-In-View interceptor. That guarantees the request's EntityManager is already
// bound when RepositoryInterceptor enables the tenant filter on it.
registry.addWebRequestInterceptor(requestInterceptor).order(Integer.MAX_VALUE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public class EnvironmentService {
private final ReleaseCandidateRepository releaseCandidateRepository;
private final DeploymentRepository deploymentRepository;
@Lazy private final GitRepoSettingsService gitRepoSettingsService;
private final EnvironmentScheduler environmentScheduler;
private final WorkflowRepository workflowRepository;
private final WorkflowRunRepository workflowRunRepository;
private final ProtectionRuleRepository protectionRuleRepository;
Expand All @@ -89,7 +88,6 @@ public List<EnvironmentDto> getAllEnvironments() {
return environmentRepository.findAllByOrderByNameAsc().stream()
.map(
environment -> {
environmentScheduler.unlockExpiredEnvironments();
LatestDeploymentUnion latest = findLatestDeployment(environment);
DeploymentDurationEstimate estimate = computeEstimate(environment);
return EnvironmentDto.fromEnvironment(
Expand All @@ -103,7 +101,6 @@ public List<EnvironmentDto> getAllEnabledEnvironments() {
return environmentRepository.findByEnabledTrueOrderByNameAsc().stream()
.map(
environment -> {
environmentScheduler.unlockExpiredEnvironments();
LatestDeploymentUnion latest = findLatestDeployment(environment);
DeploymentDurationEstimate estimate = computeEstimate(environment);
return EnvironmentDto.fromEnvironment(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
package de.tum.cit.aet.helios.filters;

import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.hibernate.Session;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;

@Component
@RequiredArgsConstructor
@Log4j2
public class RepositoryInterceptor implements WebRequestInterceptor {

public static final String X_REPOSITORY_ID = "X-REPOSITORY-ID";

private final EntityManagerFactory entityManagerFactory;

@Override
public void preHandle(@NonNull WebRequest request) {
if (request.getHeader(X_REPOSITORY_ID) != null) {
String tenantId = request.getHeader(X_REPOSITORY_ID);
RepositoryContext.setRepositoryId(tenantId);
final String tenantId = request.getHeader(X_REPOSITORY_ID);
if (tenantId == null) {
return;
}
RepositoryContext.setRepositoryId(tenantId);
final Long repositoryId = RepositoryContext.getRepositoryId();
if (repositoryId == null) {
return;
}
// Enable Hibernate's gitRepositoryFilter on this request's EntityManager so tenant-filtered
// entities are scoped to the current repository. RepositoryFilterTransactionConfig only
// enables the filter on EntityManagers created by the transaction manager, which does NOT
// happen under Open-Session-In-View (the request's transaction reuses the EM opened by OSIV).
// Without this, web reads (pull requests, environments, ...) leak across all repositories.
// This interceptor is ordered after OSIV (see SecurityConfig#corsConfigurer) so the request's
// EntityManager is already bound here.
final EntityManagerHolder holder =
(EntityManagerHolder) TransactionSynchronizationManager.getResource(entityManagerFactory);
if (holder != null) {
holder
.getEntityManager()
.unwrap(Session.class)
.enableFilter("gitRepositoryFilter")
.setParameter("repository_id", repositoryId);
} else {
log.warn(
"gitRepositoryFilter not enabled for repository {}: no EntityManager bound at preHandle",
repositoryId);
}
Comment on lines +43 to 55
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Indexes for the environments "enabled" hot path: findLatestDeployment() and the
-- release-candidate-by-(repository, commit_sha) lookup ran sequential scans per environment,
-- which pinned application-server CPU under load. Created live on prod during the 2026-07-03
-- incident and codified here. IF NOT EXISTS makes this a no-op where they already exist and
-- creates them on staging / other environments.
CREATE INDEX IF NOT EXISTS idx_deployment_env_created
ON deployment (environment_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_helios_deployment_env_created
ON helios_deployment (environment_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_release_candidate_repo_sha
ON release_candidate (repository_id, commit_sha);
Loading