-
Notifications
You must be signed in to change notification settings - Fork 17
feat(ingestion): add Executors tab with create/delete executor pools #364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
675d7bc
b714dac
ad4f0e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.linkedin.datahub.graphql.resolvers.ingest.executor; | ||
|
|
||
| import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; | ||
|
|
||
| import com.linkedin.datahub.graphql.QueryContext; | ||
| import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils; | ||
| import com.linkedin.datahub.graphql.exception.AuthorizationException; | ||
| import com.linkedin.datahub.graphql.generated.CreateExecutorPoolInput; | ||
| import com.linkedin.datahub.graphql.resolvers.ingest.IngestionAuthUtils; | ||
| import graphql.schema.DataFetcher; | ||
| import graphql.schema.DataFetchingEnvironment; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| /** | ||
| * Creates a remote executor pool. Requires the MANAGE ingestion privilege. | ||
| * | ||
| * <p>In OSS this uses an in-memory store ({@link ExecutorPoolStore}). Managed DataHub may | ||
| * override or replace this resolver to perform actual creation against an external executor service. | ||
| */ | ||
| public class CreateExecutorPoolResolver implements DataFetcher<CompletableFuture<String>> { | ||
|
|
||
| @Override | ||
| public CompletableFuture<String> get(final DataFetchingEnvironment environment) throws Exception { | ||
|
|
||
| final QueryContext context = environment.getContext(); | ||
|
|
||
| if (IngestionAuthUtils.canManageIngestion(context)) { | ||
| return GraphQLConcurrencyUtils.supplyAsync( | ||
| () -> { | ||
| final CreateExecutorPoolInput input = | ||
| bindArgument(environment.getArgument("input"), CreateExecutorPoolInput.class); | ||
| if (input == null) { | ||
| return ""; | ||
| } | ||
| String id = input.getId() != null ? input.getId() : ""; | ||
| String name = input.getName(); | ||
| return ExecutorPoolStore.create(id, name); | ||
| }, | ||
| this.getClass().getSimpleName(), | ||
| "get"); | ||
| } | ||
| throw new AuthorizationException( | ||
| "Unauthorized to perform this action. Please contact your DataHub administrator."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.linkedin.datahub.graphql.resolvers.ingest.executor; | ||
|
|
||
| import com.linkedin.datahub.graphql.QueryContext; | ||
| import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils; | ||
| import com.linkedin.datahub.graphql.exception.AuthorizationException; | ||
| import com.linkedin.datahub.graphql.resolvers.ingest.IngestionAuthUtils; | ||
| import graphql.schema.DataFetcher; | ||
| import graphql.schema.DataFetchingEnvironment; | ||
| import java.util.List; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| /** | ||
| * Deletes one or more remote executor pools by id. Requires the MANAGE ingestion privilege. | ||
| * | ||
| * <p>In OSS this removes pools from the in-memory store ({@link ExecutorPoolStore}) and returns the | ||
| * deleted ids. Managed DataHub may override or replace this resolver to perform actual deletion | ||
| * against an external executor service. | ||
| */ | ||
| public class DeleteExecutorPoolsResolver implements DataFetcher<CompletableFuture<List<String>>> { | ||
|
|
||
| @Override | ||
| public CompletableFuture<List<String>> get(final DataFetchingEnvironment environment) | ||
| throws Exception { | ||
|
|
||
| final QueryContext context = environment.getContext(); | ||
|
|
||
| if (IngestionAuthUtils.canManageIngestion(context)) { | ||
| @SuppressWarnings("unchecked") | ||
| final List<String> poolIds = environment.getArgument("poolIds"); | ||
| return GraphQLConcurrencyUtils.supplyAsync( | ||
| () -> ExecutorPoolStore.delete(poolIds != null ? poolIds : List.of()), | ||
| this.getClass().getSimpleName(), | ||
| "get"); | ||
| } | ||
| throw new AuthorizationException( | ||
| "Unauthorized to perform this action. Please contact your DataHub administrator."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.linkedin.datahub.graphql.resolvers.ingest.executor; | ||
|
|
||
| import com.linkedin.datahub.graphql.generated.ExecutorPool; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * In-memory store for executor pools. Used by OSS resolvers so create/list/delete work within a | ||
| * single JVM. Does not persist across restarts. Managed DataHub may use external storage instead. | ||
| */ | ||
| final class ExecutorPoolStore { | ||
|
|
||
| private static final Map<String, ExecutorPool> POOLS = new ConcurrentHashMap<>(); | ||
|
|
||
| static List<ExecutorPool> list(int start, int count) { | ||
| List<ExecutorPool> all = new ArrayList<>(POOLS.values()); | ||
| int total = all.size(); | ||
| int from = Math.min(start, total); | ||
| int to = Math.min(start + count, total); | ||
| return new ArrayList<>(all.subList(from, to)); | ||
| } | ||
|
||
|
|
||
| static int totalCount() { | ||
| return POOLS.size(); | ||
| } | ||
|
|
||
| static String create(String id, String name) { | ||
| if (id == null || id.isBlank()) { | ||
| return ""; | ||
| } | ||
| ExecutorPool pool = new ExecutorPool(); | ||
| pool.setId(id.trim()); | ||
| pool.setName(name != null && !name.isBlank() ? name.trim() : null); | ||
| POOLS.put(id.trim(), pool); | ||
|
||
| return id.trim(); | ||
| } | ||
|
|
||
| static List<String> delete(List<String> poolIds) { | ||
| if (poolIds == null) { | ||
| return List.of(); | ||
| } | ||
| return poolIds.stream() | ||
| .filter(Objects::nonNull) | ||
| .filter(id -> POOLS.remove(id) != null) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private ExecutorPoolStore() {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package com.linkedin.datahub.graphql.resolvers.ingest.executor; | ||
|
|
||
| import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; | ||
|
|
||
| import com.linkedin.datahub.graphql.QueryContext; | ||
| import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils; | ||
| import com.linkedin.datahub.graphql.exception.AuthorizationException; | ||
| import com.linkedin.datahub.graphql.generated.ExecutorPool; | ||
| import com.linkedin.datahub.graphql.generated.ListExecutorPoolsInput; | ||
| import com.linkedin.datahub.graphql.generated.ListExecutorPoolsResult; | ||
| import com.linkedin.datahub.graphql.resolvers.ingest.IngestionAuthUtils; | ||
| import graphql.schema.DataFetcher; | ||
| import graphql.schema.DataFetchingEnvironment; | ||
| import java.util.List; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| /** | ||
| * Lists remote executor pools. Requires the MANAGE ingestion privilege. | ||
| * | ||
| * <p>In OSS this returns pools from an in-memory store (see ExecutorPoolStore). Managed DataHub | ||
| * may override or replace this resolver to return pools from an external executor service. | ||
| */ | ||
| public class ListExecutorPoolsResolver implements DataFetcher<CompletableFuture<ListExecutorPoolsResult>> { | ||
|
|
||
| private static final Integer DEFAULT_START = 0; | ||
| private static final Integer DEFAULT_COUNT = 20; | ||
|
|
||
| @Override | ||
| public CompletableFuture<ListExecutorPoolsResult> get(final DataFetchingEnvironment environment) | ||
| throws Exception { | ||
|
|
||
| final QueryContext context = environment.getContext(); | ||
|
|
||
| if (IngestionAuthUtils.canManageIngestion(context)) { | ||
| return GraphQLConcurrencyUtils.supplyAsync( | ||
| () -> { | ||
| final ListExecutorPoolsInput input = | ||
| bindArgument(environment.getArgument("input"), ListExecutorPoolsInput.class); | ||
| if (input == null) { | ||
| final ListExecutorPoolsResult empty = new ListExecutorPoolsResult(); | ||
| empty.setStart(DEFAULT_START); | ||
| empty.setCount(DEFAULT_COUNT); | ||
| empty.setTotal(0); | ||
| empty.setPools(List.of()); | ||
| return empty; | ||
| } | ||
| final int start = input.getStart() == null ? DEFAULT_START : input.getStart(); | ||
| final int count = input.getCount() == null ? DEFAULT_COUNT : input.getCount(); | ||
|
|
||
| int total = ExecutorPoolStore.totalCount(); | ||
| List<ExecutorPool> pools = ExecutorPoolStore.list(start, count); | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| final ListExecutorPoolsResult result = new ListExecutorPoolsResult(); | ||
| result.setStart(start); | ||
| result.setCount(count); | ||
| result.setTotal(total); | ||
| result.setPools(pools); | ||
| return result; | ||
| }, | ||
| this.getClass().getSimpleName(), | ||
| "get"); | ||
| } | ||
| throw new AuthorizationException( | ||
| "Unauthorized to perform this action. Please contact your DataHub administrator."); | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unpredictable pool ordering breaks pagination consistency
Low Severity
The
ExecutorPoolStoreusesConcurrentHashMapwhich does not preserve insertion order or provide any deterministic iteration order. Pools created as A, B, C may be returned in any order (e.g., C, A, B), and this order may change between requests. This makes server-side pagination unreliable—a pool on page 1 in one request might appear on page 2 in the next.