Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ private Alert prepareAlert(Alert alert, String userName, String workspaceId) {

UUID id = alert.id() == null ? idGenerator.generateId() : alert.id();
IdGenerator.validateVersion(id, "Alert");
idGenerator.validateIdNotInFutureIfPresent(alert.projectId(), "project");

UUID webhookId = alert.webhook().id() == null ? idGenerator.generateId() : alert.webhook().id();
IdGenerator.validateVersion(webhookId, "Webhook");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public Mono<Long> addItems(@NonNull UUID queueId, @NonNull Set<UUID> itemIds) {
return Mono.just(0L);
}

// Queue items reference trace/thread ids (v7 by construction); enforce so the referenced-id
// policy is uniform. Past allowed — queues commonly collect older traces/threads.
itemIds.forEach(itemId -> idGenerator.validateIdNotInFuture(itemId, "AnnotationQueue item"));

return annotationQueueDAO.findQueueInfoById(queueId)
.switchIfEmpty(Mono.error(createNotFoundError(queueId)))
.flatMap(queue -> annotationQueueDAO.addItems(queueId, itemIds, queue.projectId()))
Expand Down Expand Up @@ -257,6 +261,7 @@ private Mono<AnnotationQueue.AnnotationQueuePage> enhancePageWithProjectNames(
private AnnotationQueue prepareAnnotationQueue(AnnotationQueue annotationQueue) {
UUID id = annotationQueue.id() == null ? idGenerator.generateId() : annotationQueue.id();
IdGenerator.validateVersion(id, "AnnotationQueue");
idGenerator.validateIdNotInFutureIfPresent(annotationQueue.projectId(), "project");

log.debug("Preparing annotation queue with id '{}', name '{}', project '{}'",
id, annotationQueue.name(), annotationQueue.projectId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class AssertionResultServiceImpl implements AssertionResultService {
private final @NonNull AssertionResultDAO assertionResultDAO;
private final @NonNull ProjectService projectService;
private final @NonNull EventBus eventBus;
private final @NonNull IdGenerator idGenerator;

@Override
public Mono<Long> insertBatch(@NonNull EntityType entityType,
Expand All @@ -63,7 +64,10 @@ public Mono<Void> saveBatch(@NonNull EntityType entityType,
}

// Validate up front so a bad id fails fast and independently of project-name normalisation.
assertionResults.forEach(item -> IdGenerator.validateVersion(item.entityId(), entityType.getType()));
assertionResults.forEach(item -> {
idGenerator.validateIdNotInFuture(item.entityId(), entityType.getType());
idGenerator.validateIdNotInFutureIfPresent(item.projectId(), "project");
});
Comment thread
thiagohora marked this conversation as resolved.
Outdated

return Mono.deferContextual(ctx -> {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CommentServiceImpl implements CommentService {

@Override
public Mono<UUID> create(@NonNull UUID entityId, @NonNull Comment comment, CommentDAO.EntityType entityType) {
idGenerator.validateIdNotInFuture(entityId, entityType.getType());
UUID id = idGenerator.generateId();
var monoProjectId = resolveProjectId(entityType, entityId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public Dashboard create(@NonNull Dashboard dashboard, @NonNull DashboardScope sc
// Generate ID if not provided
var dashboardId = dashboard.id() != null ? dashboard.id() : idGenerator.generateId();
IdGenerator.validateVersion(dashboardId, "dashboard");
idGenerator.validateIdNotInFutureIfPresent(dashboard.projectId(), "project");

final UUID resolvedProjectId;
if (StringUtils.isNotBlank(dashboard.projectName()) && dashboard.projectId() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ public Mono<Void> createFromTraces(

log.info("Creating dataset items from '{}' traces for dataset '{}'", traceIds.size(), datasetId);

traceIds.forEach(traceId -> idGenerator.validateIdNotInFuture(traceId, "dataset_item trace"));

// Verify dataset exists
return Mono.deferContextual(ctx -> {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
Expand Down Expand Up @@ -240,6 +242,8 @@ public Mono<Void> createFromSpans(

log.info("Creating dataset items from '{}' spans for dataset '{}'", spanIds.size(), datasetId);

spanIds.forEach(spanId -> idGenerator.validateIdNotInFuture(spanId, "dataset_item span"));

// Verify dataset exists
return Mono.deferContextual(ctx -> {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
Expand Down Expand Up @@ -382,6 +386,7 @@ private Mono<DatasetItem> authorizeItem(Mono<DatasetItem> itemMono) {
@Override
@WithSpan
public Mono<Void> patch(@NonNull UUID id, @NonNull DatasetItem item) {
validateReferencedTraceAndSpan(item);
return Mono.deferContextual(ctx -> {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
String userName = ctx.get(RequestContext.USER_NAME);
Expand Down Expand Up @@ -903,11 +908,23 @@ private List<DatasetItem> addIdIfAbsent(DatasetItemBatch batch) {
.stream()
.map(item -> {
IdGenerator.validateVersion(item.id(), "dataset_item");
validateReferencedTraceAndSpan(item);
return item;
})
.toList();
}

// The dataset_item's referenced trace_id / span_id must be a time-ordered UUIDv7 (past allowed:
// items are commonly linked to older traces/spans). Reuses the shared referenced-id policy.
private void validateReferencedTraceAndSpan(DatasetItem item) {
if (item.traceId() != null) {
idGenerator.validateIdNotInFuture(item.traceId(), "dataset_item trace");
}
if (item.spanId() != null) {
idGenerator.validateIdNotInFuture(item.spanId(), "dataset_item span");
}
}

private <T> Mono<T> failWithConflict(String message) {
log.info(message);
return Mono.error(new IdentifierMismatchException(new ErrorMessage(List.of(message))));
Expand Down Expand Up @@ -1555,6 +1572,10 @@ private List<DatasetItem> prepareAddedItems(DatasetItemChanges changes, UUID dat
@WithSpan
public Mono<DatasetVersion> save(@NonNull DatasetItemBatch batch) {

idGenerator.validateIdNotInFutureIfPresent(batch.datasetId(), "dataset");
idGenerator.validateIdNotInFutureIfPresent(batch.copyFromDatasetId(), "dataset");
idGenerator.validateIdNotInFutureIfPresent(batch.copyFromVersionId(), "dataset version");

if (!featureFlags.isDatasetVersioningEnabled()) {
// Legacy: save to legacy table
log.info("Saving items to legacy table for dataset '{}'", batch.datasetId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ private Set<UUID> getPromptVersionIds(Experiment experiment) {
public Mono<UUID> create(@NonNull Experiment experiment) {
var id = experiment.id() == null ? idGenerator.generateId() : experiment.id();
IdGenerator.validateVersion(id, "Experiment");
idGenerator.validateIdNotInFutureIfPresent(experiment.projectId(), "project");
idGenerator.validateIdNotInFutureIfPresent(experiment.optimizationId(), "optimization");
idGenerator.validateIdNotInFutureIfPresent(experiment.datasetVersionId(), "dataset version");
Comment thread
thiagohora marked this conversation as resolved.
Outdated
var name = StringUtils.getIfBlank(experiment.name(), nameGenerator::generateName);
return resolveProjectId(experiment)
.flatMap(resolvedExperiment -> datasetService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class FeedbackScoreServiceImpl implements FeedbackScoreService {
private final @NonNull TraceThreadService traceThreadService;
private final @NonNull Provider<RequestContext> requestContext;
private final @NonNull EventBus eventBus;
private final @NonNull IdGenerator idGenerator;

@Builder(toBuilder = true)
record ProjectDto<T extends FeedbackScoreItem>(Project project, List<T> scores) {
Expand All @@ -100,6 +101,8 @@ public Mono<Void> scoreTrace(@NonNull UUID traceId, @NonNull FeedbackScore score
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
String userName = ctx.get(RequestContext.USER_NAME);

idGenerator.validateIdNotInFuture(traceId, EntityType.TRACE.getType());
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");
return traceDAO.getProjectIdFromTrace(traceId)
.switchIfEmpty(Mono.error(failWithNotFound("Trace", traceId)))
.flatMap(projectId -> getAuthor()
Expand All @@ -118,6 +121,8 @@ public Mono<Void> scoreSpan(@NonNull UUID spanId, @NonNull FeedbackScore score)
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
String userName = ctx.get(RequestContext.USER_NAME);

idGenerator.validateIdNotInFuture(spanId, EntityType.SPAN.getType());
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");
return spanDAO.getProjectIdFromSpan(spanId)
.switchIfEmpty(Mono.error(failWithNotFound("Span", spanId)))
.flatMap(projectId -> getAuthor()
Expand Down Expand Up @@ -173,7 +178,9 @@ private Mono<Void> processScoreBatch(EntityType entityType, List<FeedbackScoreBa
Map<String, List<FeedbackScoreItem>> scoresPerProject = scores
.stream()
.map(score -> {
IdGenerator.validateVersion(score.id(), entityType.getType()); // validate span/trace id
idGenerator.validateIdNotInFuture(score.id(), entityType.getType()); // validate span/trace id
idGenerator.validateIdNotInFutureIfPresent(score.projectId(), "project");
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");

return score.toBuilder()
.projectName(WorkspaceUtils.getProjectName(score.projectName()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public Mono<Void> addTraceGuardrails(List<Guardrail> guardrails) {
.stream()
.map(guardrail -> {
UUID id = idGenerator.generateId();
IdGenerator.validateVersion(guardrail.entityId(), entityType.getType()); // validate trace id
idGenerator.validateIdNotInFuture(guardrail.entityId(), entityType.getType());
idGenerator.validateIdNotInFuture(guardrail.secondaryId(), "guardrail secondary");
idGenerator.validateIdNotInFutureIfPresent(guardrail.projectId(), "project");

Comment thread
thiagohora marked this conversation as resolved.
return guardrail.toBuilder()
.id(id)
Expand Down
Comment thread
thiagohora marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,33 @@ public interface IdGenerator {
Mono<UUID> validateIdAsync(UUID id, String resource);

/**
* Validates an ingested {@code id} on the update path: it must be a version 7 UUID
* ({@link #validateVersion(UUID, String)}) and must not embed a timestamp far in the future (which
* would corrupt the partition layout). Unlike {@link #validateId}, old ids are allowed, because
* updating a long-lived entity (e.g. created months ago) is a legitimate operation.
* Validates an {@code id} that may legitimately point at an entity created in the past: it must be a
* version 7 UUID ({@link #validateVersion(UUID, String)}) and must not embed a timestamp far in the
* future (which would corrupt the partition layout / retention id-range). Unlike {@link #validateId},
* old ids are allowed.
*
* <p>Used both on the update path (updating a long-lived entity created months ago is legitimate) and
* for referenced/foreign ids on ingest (e.g. a span's {@code traceId}: retention orders spans by the
* {@code trace_id} range assuming it is a time-ordered UUIDv7, and late spans on old traces are common,
* so old is fine but non-v7 or future-dated must be rejected).
*/
Mono<UUID> validateIdForUpdateAsync(UUID id, String resource);
void validateIdNotInFuture(UUID id, String resource);

Mono<UUID> validateIdNotInFutureAsync(UUID id, String resource);

/**
* Null-safe variant of {@link #validateIdNotInFuture} for optional referenced ids (e.g. an optional
* {@code projectId} that may be resolved by name instead). No-op when {@code id} is null.
*/
default void validateIdNotInFutureIfPresent(UUID id, String resource) {
Comment thread
thiagohora marked this conversation as resolved.
Outdated
if (id != null) {
validateIdNotInFuture(id, resource);
}
}

default Mono<UUID> validateIdNotInFutureIfPresentAsync(UUID id, String resource) {
return id == null ? Mono.empty() : validateIdNotInFutureAsync(id, resource);
}

static Mono<UUID> validateVersionAsync(@NonNull UUID id, String resource) {
return Mono.fromCallable(() -> {
Expand Down Expand Up @@ -91,15 +112,16 @@ public Mono<UUID> validateIdAsync(@NonNull UUID id, String resource) {
});
}

private void validateIdForUpdate(UUID id, String resource) {
@Override
public void validateIdNotInFuture(@NonNull UUID id, String resource) {
IdGenerator.validateVersion(id, resource);
uuidV7TimestampValidator.validateNotInFuture(id);
}

@Override
public Mono<UUID> validateIdForUpdateAsync(@NonNull UUID id, String resource) {
public Mono<UUID> validateIdNotInFutureAsync(@NonNull UUID id, String resource) {
return Mono.fromCallable(() -> {
validateIdForUpdate(id, resource);
validateIdNotInFuture(id, resource);
return id;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ private OptimizationSearchCriteria resolveDatasetNameFilter(
public Mono<UUID> upsert(@NonNull Optimization optimization) {
UUID id = optimization.id() == null ? idGenerator.generateId() : optimization.id();
IdGenerator.validateVersion(id, "Optimization");
idGenerator.validateIdNotInFutureIfPresent(optimization.projectId(), "project");

// Detect if this is a Studio optimization (has studioConfig in the request)
boolean isStudioOptimization = optimization.studioConfig() != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ private PromptVersion createPromptVersionFromPromptRequest(Prompt createdPrompt,
private Prompt savePrompt(String workspaceId, Prompt prompt) {

IdGenerator.validateVersion(prompt.id(), "prompt");
idGenerator.validateIdNotInFutureIfPresent(prompt.projectId(), "project");

transactionTemplate.inTransaction(WRITE, handle -> {
PromptDAO promptDAO = handle.attach(PromptDAO.class);
Expand Down Expand Up @@ -332,6 +333,7 @@ public PromptVersion createPromptVersion(@NonNull CreatePromptVersion createProm
: createPromptVersion.version().commit();

IdGenerator.validateVersion(id, "prompt version");
idGenerator.validateIdNotInFutureIfPresent(createPromptVersion.projectId(), "project");

TemplateStructure templateStructure = createPromptVersion.templateStructure();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public class SpanService {
public static final String PARENT_SPAN_IS_MISMATCH = "parent_span_id does not match the existing span";
public static final String TRACE_ID_MISMATCH = "trace_id does not match the existing span";
public static final String SPAN_KEY = "Span";
public static final String SPAN_TRACE_KEY = "Span trace";
public static final String SPAN_PARENT_KEY = "Span parent";
public static final String PROJECT_AND_WORKSPACE_NAME_MISMATCH = "Project name and workspace name do not match the existing span";

private final @NonNull SpanDAO spanDAO;
Expand Down Expand Up @@ -159,6 +161,8 @@ public Mono<UUID> create(@NonNull Span span) {
var projectName = WorkspaceUtils.getProjectName(span.projectName());
return idGenerator
.validateIdAsync(id, SPAN_KEY)
.then(idGenerator.validateIdNotInFutureAsync(span.traceId(), SPAN_TRACE_KEY))
.then(idGenerator.validateIdNotInFutureIfPresentAsync(span.parentSpanId(), SPAN_PARENT_KEY))
.then(projectService.getOrCreate(projectName))
Comment thread
thiagohora marked this conversation as resolved.
.flatMap(project -> lockService.executeWithLock(
new LockService.Lock(id, SPAN_KEY),
Expand Down Expand Up @@ -220,7 +224,10 @@ public Mono<Void> update(@NonNull UUID id, @NonNull SpanUpdate spanUpdate) {
String userName = ctx.get(RequestContext.USER_NAME);

return idGenerator
.validateIdForUpdateAsync(id, SPAN_KEY)
.validateIdNotInFutureAsync(id, SPAN_KEY)
.then(idGenerator.validateIdNotInFutureAsync(spanUpdate.traceId(), SPAN_TRACE_KEY))
.then(idGenerator.validateIdNotInFutureIfPresentAsync(spanUpdate.parentSpanId(), SPAN_PARENT_KEY))
.then(idGenerator.validateIdNotInFutureIfPresentAsync(spanUpdate.projectId(), "project"))
.then(Mono.defer(() -> getProjectById(spanUpdate)
.switchIfEmpty(Mono.defer(() -> projectService.getOrCreate(projectName)))
.subscribeOn(Schedulers.boundedElastic()))
Expand All @@ -247,7 +254,11 @@ public Mono<Void> batchUpdate(@NonNull SpanBatchUpdate batchUpdate) {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
String userName = ctx.get(RequestContext.USER_NAME);

return spanDAO.bulkUpdate(batchUpdate.ids(), batchUpdate.update(), mergeTags)
return idGenerator.validateIdNotInFutureAsync(batchUpdate.update().traceId(), SPAN_TRACE_KEY)
.then(idGenerator.validateIdNotInFutureIfPresentAsync(batchUpdate.update().parentSpanId(),
SPAN_PARENT_KEY))
.then(idGenerator.validateIdNotInFutureIfPresentAsync(batchUpdate.update().projectId(), "project"))
.then(spanDAO.bulkUpdate(batchUpdate.ids(), batchUpdate.update(), mergeTags))
.onErrorResume(TagOperations::mapTagLimitError)
.doOnSuccess(__ -> {
log.info("Completed batch update for '{}' spans", batchUpdate.ids().size());
Expand Down Expand Up @@ -460,6 +471,8 @@ private List<Span> bindSpanToProjectAndId(List<Span> spans, List<Project> projec

UUID id = span.id() == null ? idGenerator.generateId() : span.id();
idGenerator.validateId(id, SPAN_KEY);
idGenerator.validateIdNotInFuture(span.traceId(), SPAN_TRACE_KEY);
idGenerator.validateIdNotInFutureIfPresent(span.parentSpanId(), SPAN_PARENT_KEY);
Comment thread
thiagohora marked this conversation as resolved.
Outdated

return span.toBuilder().id(id).projectId(project.id()).build();
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ public Mono<Void> update(@NonNull TraceUpdate traceUpdate, @NonNull UUID id) {
var projectName = WorkspaceUtils.getProjectName(traceUpdate.projectName());

return Mono.deferContextual(ctx -> idGenerator
.validateIdForUpdateAsync(id, TRACE_KEY)
.validateIdNotInFutureAsync(id, TRACE_KEY)
.then(idGenerator.validateIdNotInFutureIfPresentAsync(traceUpdate.projectId(), "project"))
.then(getProjectById(traceUpdate)
.switchIfEmpty(Mono.defer(() -> projectService.getOrCreate(projectName)))
.subscribeOn(Schedulers.boundedElastic())
Expand Down Expand Up @@ -358,7 +359,8 @@ public Mono<Void> batchUpdate(@NonNull TraceBatchUpdate batchUpdate) {
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);
String userName = ctx.get(RequestContext.USER_NAME);
String workspaceName = ctx.getOrDefault(RequestContext.WORKSPACE_NAME, "");
return dao.getProjectIdsByTraceIds(new ArrayList<>(batchUpdate.ids()))
return idGenerator.validateIdNotInFutureIfPresentAsync(batchUpdate.update().projectId(), "project")
.then(dao.getProjectIdsByTraceIds(new ArrayList<>(batchUpdate.ids())))
.flatMap(traceToProjectMap -> {
var projectIds = Set.copyOf(traceToProjectMap.values());
return dao.bulkUpdate(batchUpdate.ids(), batchUpdate.update(), mergeTags)
Expand Down
Loading
Loading