diff --git a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java index b70c4c5ed..48468a65f 100644 --- a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java +++ b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java @@ -12,8 +12,7 @@ */ package com.netflix.conductor.core.execution; -import java.util.List; - +import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.tasks.TaskResult; import com.netflix.conductor.common.metadata.workflow.RerunWorkflowRequest; import com.netflix.conductor.common.metadata.workflow.SkipTaskRequest; @@ -23,6 +22,9 @@ import com.netflix.conductor.model.TaskModel; import com.netflix.conductor.model.WorkflowModel; +import java.util.List; +import java.util.Map; + public interface WorkflowExecutor { /** @@ -105,6 +107,12 @@ void restart(String workflowId, boolean useLatestDefinitions) */ WorkflowModel decide(String workflowId); + /** + * @param workflow the workflow to be evaluated + * @return updated workflow + */ + WorkflowModel decide(WorkflowModel workflow); + /** * @param workflowId id of the workflow to be terminated * @param reason termination reason to be recorded @@ -159,4 +167,23 @@ void skipTaskFromWorkflow( * @return id of the workflow */ String startWorkflow(StartWorkflowInput input); + + default String startWorkflow(String name, Integer version, + String correlationId, + Map workflowInput, + String externalInputPayloadStoragePath, String event, + Map taskToDomain) { + StartWorkflowInput input = new StartWorkflowInput(); + input.setName(name); + input.setVersion(version); + input.setCorrelationId(correlationId); + input.setWorkflowInput(workflowInput); + input.setExternalInputPayloadStoragePath(externalInputPayloadStoragePath); + input.setEvent(event); + input.setTaskToDomain(taskToDomain); + return startWorkflow(input); + } + + TaskDef getTaskDefinition(TaskModel task); + public void addTaskToQueue(TaskModel task); } diff --git a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutorOps.java b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutorOps.java index 77d5ec1b3..6e123bd43 100644 --- a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutorOps.java +++ b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutorOps.java @@ -1045,7 +1045,8 @@ public WorkflowModel decide(String workflowId) { * method does not acquire the lock on the workflow and should ony be called / overridden if * No locking is required or lock is acquired externally */ - private WorkflowModel decide(WorkflowModel workflow) { + @Override + public WorkflowModel decide(WorkflowModel workflow) { if (workflow.getStatus().isTerminal()) { if (!workflow.getStatus().isSuccessful()) { cancelNonTerminalTasks(workflow); @@ -1371,7 +1372,8 @@ public WorkflowModel getWorkflow(String workflowId, boolean includeTasks) { return executionDAOFacade.getWorkflowModel(workflowId, includeTasks); } - private void addTaskToQueue(TaskModel task) { + @Override + public void addTaskToQueue(TaskModel task) { // put in queue String taskQueueName = QueueUtils.getQueueName(task); if (task.getCallbackAfterSeconds() > 0) { @@ -1767,7 +1769,8 @@ public void scheduleNextIteration(TaskModel loopTask, WorkflowModel workflow) { workflow.getTasks().addAll(scheduledLoopOverTasks); } - private TaskDef getTaskDefinition(TaskModel task) { + @Override + public TaskDef getTaskDefinition(TaskModel task) { return task.getTaskDefinition() .orElseGet( () -> diff --git a/dependencies.gradle b/dependencies.gradle index b4ebff849..7fc218c12 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -34,6 +34,7 @@ ext { revHealth = '1.1.4' revPostgres = '42.7.2' revProtoBuf = '3.25.5' + revProtoBufUtils = '4.28.3' revJakartaAnnotation = '2.1.1' revJAXB = '4.0.1' revJAXRS = '4.0.0' diff --git a/grpc-task/build.gradle b/grpc-task/build.gradle new file mode 100644 index 000000000..8fb794e0f --- /dev/null +++ b/grpc-task/build.gradle @@ -0,0 +1,18 @@ +dependencies { + implementation project(':conductor-common') + implementation project(':conductor-core') + + compileOnly 'org.springframework.boot:spring-boot-starter' + + implementation "io.grpc:grpc-netty:${revGrpc}" + implementation "io.grpc:grpc-services:${revGrpc}" + implementation "io.grpc:grpc-protobuf:${revGrpc}" + implementation "com.google.protobuf:protobuf-java:${revProtoBuf}" + implementation "com.google.protobuf:protobuf-java-util:${revProtoBufUtils}" + + implementation "com.fasterxml.jackson.core:jackson-databind" + implementation "com.fasterxml.jackson.core:jackson-core" + implementation "com.fasterxml.jackson.core:jackson-annotations" + + testImplementation "io.grpc:grpc-protobuf:${revGrpc}" +} diff --git a/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicMessageMarshaller.java b/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicMessageMarshaller.java new file mode 100644 index 000000000..22d2886ee --- /dev/null +++ b/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicMessageMarshaller.java @@ -0,0 +1,30 @@ +package org.conductoross.tasks.grpc; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import io.grpc.MethodDescriptor; + +import java.io.IOException; +import java.io.InputStream; + +public class DynamicMessageMarshaller implements MethodDescriptor.Marshaller { + private final Descriptors.Descriptor descriptor; + + public DynamicMessageMarshaller(Descriptors.Descriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public InputStream stream(DynamicMessage value) { + return value.toByteString().newInput(); + } + + @Override + public DynamicMessage parse(InputStream stream) { + try { + return DynamicMessage.parseFrom(descriptor, stream); + } catch (IOException e) { + throw new RuntimeException("Failed to parse message", e); + } + } +} diff --git a/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicServiceDescriptorBuilder.java b/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicServiceDescriptorBuilder.java new file mode 100644 index 000000000..10fe75f2b --- /dev/null +++ b/grpc-task/src/main/java/org/conductoross/tasks/grpc/DynamicServiceDescriptorBuilder.java @@ -0,0 +1,119 @@ +package org.conductoross.tasks.grpc; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class DynamicServiceDescriptorBuilder { + + private final Map fileDescriptorMap = new HashMap<>(); + + /** + * Loads a descriptor set file and initializes the FileDescriptors. + * + * @param descriptorSetPath Path to the .desc file. + * @throws IOException If the file cannot be read. + * @throws Descriptors.DescriptorValidationException If descriptor validation fails. + */ + public void loadDescriptorSet(String descriptorSetPath) throws IOException, Descriptors.DescriptorValidationException { + DescriptorProtos.FileDescriptorSet descriptorSet = + DescriptorProtos.FileDescriptorSet.parseFrom(new FileInputStream(descriptorSetPath)); + + // Add well-known types to the descriptor cache + addWellKnownTypes(); + + // Build each FileDescriptor and cache it in the map + for (DescriptorProtos.FileDescriptorProto fileDescriptorProto : descriptorSet.getFileList()) { + // Resolve dependencies + Descriptors.FileDescriptor[] dependencies = fileDescriptorProto.getDependencyList().stream() + .map(fileDescriptorMap::get) + .toArray(Descriptors.FileDescriptor[]::new); + + // Build the FileDescriptor + Descriptors.FileDescriptor fileDescriptor = + Descriptors.FileDescriptor.buildFrom(fileDescriptorProto, dependencies); + + // Cache the FileDescriptor for resolving future imports + fileDescriptorMap.put(fileDescriptor.getName(), fileDescriptor); + } + } + + /** + * Adds well-known Protobuf types (e.g., Timestamp, Any) to the descriptor map. + */ + private void addWellKnownTypes() { + // Add each well-known type to the file descriptor map + fileDescriptorMap.put("google/protobuf/timestamp.proto", com.google.protobuf.TimestampProto.getDescriptor()); + fileDescriptorMap.put("google/protobuf/any.proto", com.google.protobuf.AnyProto.getDescriptor()); + fileDescriptorMap.put("google/protobuf/struct.proto", com.google.protobuf.StructProto.getDescriptor()); + fileDescriptorMap.put("google/protobuf/duration.proto", com.google.protobuf.DurationProto.getDescriptor()); + fileDescriptorMap.put("google/protobuf/empty.proto", com.google.protobuf.EmptyProto.getDescriptor()); + fileDescriptorMap.put("google/protobuf/field_mask.proto", com.google.protobuf.FieldMaskProto.getDescriptor()); + } + + public Descriptors.Descriptor getMessageType(String name) { + for (Descriptors.FileDescriptor fd : fileDescriptorMap.values()) { + for (Descriptors.Descriptor messageType : fd.getMessageTypes()) { + String messageTypeName = messageType.getName(); + String fullName = messageType.getFullName(); + System.out.println(messageTypeName + " == " + fullName); + if(fullName.equals(name)) { + return messageType; + } + } + } + return null; + } + + public String getMessageProto(String name) { + var descriptor = getMessageType(name); + return getProtoDefinition(descriptor, "\t"); + } + + private String getProtoDefinition(Descriptors.Descriptor descriptor, String indent) { + StringBuilder protoDef = new StringBuilder(); + protoDef.append(indent).append("message ").append(descriptor.getName()).append(" {\n"); + + for (Descriptors.FieldDescriptor field : descriptor.getFields()) { + protoDef.append(indent).append(" "); + protoDef.append(getFieldDefinition(field)).append("\n"); + } + + for (Descriptors.Descriptor nestedDescriptor : descriptor.getNestedTypes()) { + protoDef.append(getProtoDefinition(nestedDescriptor, indent + " ")); + } + + protoDef.append(indent).append("}\n"); + return protoDef.toString(); + } + + private String getFieldDefinition(Descriptors.FieldDescriptor field) { + StringBuilder fieldDef = new StringBuilder(); + + // Add field type + if (field.isRepeated()) { + fieldDef.append("repeated "); + } + + switch (field.getType()) { + case MESSAGE: + fieldDef.append(field.getMessageType().getName()); + break; + case ENUM: + fieldDef.append(field.getEnumType().getName()); + break; + default: + fieldDef.append(field.getType().name().toLowerCase()); + } + + // Add field name and number + fieldDef.append(" ").append(field.getName()).append(" = ").append(field.getNumber()).append(";"); + + return fieldDef.toString(); + } +} + diff --git a/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcDynamicCaller.java b/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcDynamicCaller.java new file mode 100644 index 000000000..a7c1ddaf0 --- /dev/null +++ b/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcDynamicCaller.java @@ -0,0 +1,105 @@ +package org.conductoross.tasks.grpc; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.stub.ClientCalls; + +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class GrpcDynamicCaller { + + private final ManagedChannel channel; + + public GrpcDynamicCaller(ManagedChannel channel) { + this.channel = channel; + } + + public DynamicMessage callUnaryMethod( + String fullMethodName, + Descriptors.Descriptor inputDescriptor, + Descriptors.Descriptor outputDescriptor, + DynamicMessage requestMessage + ) { + // Build the gRPC MethodDescriptor dynamically + MethodDescriptor grpcMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName(fullMethodName) + .setRequestMarshaller(new DynamicMessageMarshaller(inputDescriptor)) + .setResponseMarshaller(new DynamicMessageMarshaller(outputDescriptor)) + .build(); + + // Make the call + return ClientCalls.blockingUnaryCall(channel, grpcMethodDescriptor, io.grpc.CallOptions.DEFAULT, requestMessage); + } + + public Stream callResponseStreamMethod( + String fullMethodName, + Descriptors.Descriptor inputDescriptor, + Descriptors.Descriptor outputDescriptor, + DynamicMessage requestMessage + ) { + // Build the gRPC MethodDescriptor dynamically + MethodDescriptor grpcMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.SERVER_STREAMING) + .setFullMethodName(fullMethodName) + .setRequestMarshaller(new DynamicMessageMarshaller(inputDescriptor)) + .setResponseMarshaller(new DynamicMessageMarshaller(outputDescriptor)) + .build(); + + // Make the call + var stream = ClientCalls.blockingServerStreamingCall(channel, grpcMethodDescriptor, io.grpc.CallOptions.DEFAULT, requestMessage); + return toStream(stream); + } + + public DynamicMessage callUnaryMethod( + Descriptors.ServiceDescriptor serviceDescriptor, + String methodName, + DynamicMessage requestMessage + ) { + // Find the method descriptor + Descriptors.MethodDescriptor methodDescriptor = serviceDescriptor.findMethodByName(methodName); + if (methodDescriptor == null) { + throw new IllegalArgumentException("Method " + methodName + " not found in service " + serviceDescriptor.getFullName()); + } + + // Build the gRPC method descriptor + MethodDescriptor grpcMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + MethodDescriptor.generateFullMethodName(serviceDescriptor.getFullName(), methodName)) + .setRequestMarshaller(new DynamicMessageMarshaller(methodDescriptor.getInputType())) + .setResponseMarshaller(new DynamicMessageMarshaller(methodDescriptor.getOutputType())) + .build(); + + // Make the call + return ClientCalls.blockingUnaryCall(channel, grpcMethodDescriptor, io.grpc.CallOptions.DEFAULT, requestMessage); + } + + public static DynamicMessage jsonToProto(String json, Descriptors.Descriptor descriptor) throws InvalidProtocolBufferException { + DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor); + JsonFormat.parser().merge(json, builder); + return builder.build(); + } + + private static Stream toStream(Iterator iterator) { + return StreamSupport.stream( + ((Iterable) () -> iterator).spliterator(), + false + ); + } + + + + + + +} diff --git a/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcReflectionUtil.java b/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcReflectionUtil.java new file mode 100644 index 000000000..4ea3165c2 --- /dev/null +++ b/grpc-task/src/main/java/org/conductoross/tasks/grpc/GrpcReflectionUtil.java @@ -0,0 +1,101 @@ +package org.conductoross.tasks.grpc; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import io.grpc.ManagedChannel; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class GrpcReflectionUtil { + + private final ServerReflectionGrpc.ServerReflectionStub reflectionStub; + private final Map serviceCache = new HashMap<>(); + + public GrpcReflectionUtil(ManagedChannel channel) { + this.reflectionStub = ServerReflectionGrpc.newStub(channel); + } + + public Descriptors.ServiceDescriptor getServiceDescriptor(String serviceName) { + // Check cache + if (serviceCache.containsKey(serviceName)) { + return serviceCache.get(serviceName); + } + + CountDownLatch latch = new CountDownLatch(1); + final Descriptors.ServiceDescriptor[] resultHolder = new Descriptors.ServiceDescriptor[1]; + final Exception[] errorHolder = new Exception[1]; + + StreamObserver requestObserver = reflectionStub.serverReflectionInfo(new StreamObserver<>() { + @Override + public void onNext(ServerReflectionResponse response) { + if (response.hasFileDescriptorResponse()) { + try { + for (var descriptorProtoBytes : response.getFileDescriptorResponse().getFileDescriptorProtoList()) { + DescriptorProtos.FileDescriptorProto fileDescriptorProto = + DescriptorProtos.FileDescriptorProto.parseFrom(descriptorProtoBytes); + + Descriptors.FileDescriptor fileDescriptor = + Descriptors.FileDescriptor.buildFrom(fileDescriptorProto, new Descriptors.FileDescriptor[]{}); + + for (Descriptors.ServiceDescriptor serviceDescriptor : fileDescriptor.getServices()) { + if (serviceDescriptor.getFullName().equals(serviceName)) { + serviceCache.put(serviceName, serviceDescriptor); + resultHolder[0] = serviceDescriptor; + latch.countDown(); + return; + } + } + } + } catch (Exception e) { + errorHolder[0] = e; + latch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) { + errorHolder[0] = new RuntimeException("Error during reflection: " + t.getMessage(), t); + latch.countDown(); + } + + @Override + public void onCompleted() { + latch.countDown(); + } + }); + + ServerReflectionRequest request = ServerReflectionRequest.newBuilder() + .setFileContainingSymbol(serviceName) + .build(); + + requestObserver.onNext(request); + requestObserver.onCompleted(); + + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + throw new RuntimeException("Reflection request timed out"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Reflection request interrupted", e); + } + + if (errorHolder[0] != null) { + throw new RuntimeException("Reflection error", errorHolder[0]); + } + + if (resultHolder[0] == null) { + throw new RuntimeException("Service descriptor not found for: " + serviceName); + } + + return resultHolder[0]; + } +} diff --git a/grpc-task/src/test/java/org/conductoross/tasks/grpc/GrpcDynamicCallerTest.java b/grpc-task/src/test/java/org/conductoross/tasks/grpc/GrpcDynamicCallerTest.java new file mode 100644 index 000000000..5b1cc84e5 --- /dev/null +++ b/grpc-task/src/test/java/org/conductoross/tasks/grpc/GrpcDynamicCallerTest.java @@ -0,0 +1,70 @@ +package org.conductoross.tasks.grpc; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.MethodDescriptor; +import io.grpc.stub.ClientCalls; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.conductoross.tasks.grpc.GrpcDynamicCaller.jsonToProto; + +public class GrpcDynamicCallerTest { + + public static void main(String[] args) throws Exception { + + // Use https://github.com/conductor-oss/grpcbin to bring up the server + ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051) + .usePlaintext() + .build(); + + try { + var classLoader = GrpcDynamicCallerTest.class.getClassLoader(); + String file = classLoader.getResource("./compiled_protos.desc").getFile(); + Path filePath = Paths.get(classLoader.getResource("input.json").toURI()); + String jsonString = Files.readString(filePath); + + DynamicServiceDescriptorBuilder builder = new DynamicServiceDescriptorBuilder(); + builder.loadDescriptorSet(file); + + Descriptors.Descriptor messageType = builder.getMessageType( "complex.ComplexMessage"); + DynamicMessage requestMessage = jsonToProto(jsonString, messageType); + + GrpcDynamicCaller dynamicCaller = new GrpcDynamicCaller(channel); + + DynamicMessage response = dynamicCaller.callUnaryMethod("helloworld.HelloWorldService/ComplexRequest", + messageType, messageType, requestMessage); + + String json = JsonFormat.printer().print(response); + System.out.println("Response(2): " + json); + + + Stream responseStream = dynamicCaller.callResponseStreamMethod("helloworld.HelloWorldService/ComplexRequestStream", + messageType, messageType, requestMessage); + List responses = responseStream.toList(); + for (DynamicMessage res : responses) { + json = JsonFormat.printer().print(res); + System.out.println("Response(XX): " + json); + } + + } finally { + channel.shutdown(); + } + } + + + + +} diff --git a/grpc-task/src/test/resources/compiled_protos.desc b/grpc-task/src/test/resources/compiled_protos.desc new file mode 100644 index 000000000..e1a0a2e8b Binary files /dev/null and b/grpc-task/src/test/resources/compiled_protos.desc differ diff --git a/grpc-task/src/test/resources/input.json b/grpc-task/src/test/resources/input.json new file mode 100644 index 000000000..8303f9c6a --- /dev/null +++ b/grpc-task/src/test/resources/input.json @@ -0,0 +1,117 @@ +{ + "id": 123, + "name": "Test ComplexMessage", + "nested_list": [ + { + "key": "nested_key_1", + "values": ["value1", "value2"] + }, + { + "key": "nested_key_2", + "values": ["value3", "value4"] + } + ], + "nested_map": { + "map_key_1": { + "key": "map_nested_key_1", + "values": ["nested_map_value1", "nested_map_value2"] + }, + "map_key_2": { + "key": "map_nested_key_2", + "values": ["nested_map_value3", "nested_map_value4"] + } + }, + "map_of_lists": { + "list_key_1": { + "values": ["list_value1", "list_value2"] + }, + "list_key_2": { + "values": ["list_value3", "list_value4"] + } + }, + "map_of_maps": { + "outer_key_1": { + "entries": { + "inner_key_1": 100, + "inner_key_2": 200 + } + }, + "outer_key_2": { + "entries": { + "inner_key_3": 300, + "inner_key_4": 400 + } + } + }, + "map_of_message_lists": { + "message_list_key_1": { + "items": [ + { + "description": "SubMessage 1", + "timestamp": "2023-07-21T15:33:20.123Z" + }, + { + "description": "SubMessage 2", + "timestamp": "2023-07-21T15:33:20.123Z" + } + ] + } + }, + "nested_map_message_list": [ + { + "outer_key": "outer_key_1", + "inner_map": { + "inner_map_key_1": { + "values": ["inner_map_value1", "inner_map_value2"] + }, + "inner_map_key_2": { + "values": ["inner_map_value3", "inner_map_value4"] + } + } + } + ], + "deeply_nested_list": [ + { + "id": "deeply_nested_1", + "complex_map": { + "deep_key_1": { + "entries": { + "key_1": 500, + "key_2": 600 + } + } + }, + "complex_list": [ + { + "outer_key": "deep_outer_key_1", + "inner_map": { + "deep_inner_key_1": { + "values": ["deep_value1", "deep_value2"] + } + } + } + ] + } + ], + "created_at": "2023-07-21T15:33:20.123Z", + "current_status": "ACTIVE", + "special_string": "This is a special string", + "repeated_maps": [ + { + "data": { + "rep_map_key_1": 700, + "rep_map_key_2": 800 + } + }, + { + "data": { + "rep_map_key_3": 900, + "rep_map_key_4": 1000 + } + } + ], + "status_map": { + "status_key_1": "ACTIVE", + "status_key_2": "INACTIVE" + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 605765ede..a033284a9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -52,6 +52,7 @@ include 'redis-concurrency-limit' include 'json-jq-task' include 'http-task' +include 'grpc-task' include 'rest' include 'grpc'