All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xyz.block.ftl.runtime.FTLController Maven / Gradle / Ivy

There is a newer version: 0.368.1
Show newest version
package xyz.block.ftl.runtime;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;

import com.google.protobuf.ByteString;

import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import xyz.block.ftl.LeaseClient;
import xyz.block.ftl.LeaseFailedException;
import xyz.block.ftl.LeaseHandle;
import xyz.block.ftl.v1.*;
import xyz.block.ftl.v1.schema.Ref;

public class FTLController implements LeaseClient {
    private static final Logger log = Logger.getLogger(FTLController.class);
    final String moduleName;

    private Throwable currentError;
    private volatile ModuleContextResponse moduleContextResponse;
    private boolean waiters = false;

    final VerbServiceGrpc.VerbServiceStub verbService;
    final ModuleServiceGrpc.ModuleServiceStub moduleService;
    final StreamObserver moduleObserver = new ModuleObserver();

    private static volatile FTLController controller;

    /**
     * TODO: look at how init should work, this is terrible and will break dev mode
     */
    public static FTLController instance() {
        if (controller == null) {
            synchronized (FTLController.class) {
                if (controller == null) {
                    controller = new FTLController();
                }
            }
        }
        return controller;
    }

    FTLController() {
        String endpoint = System.getenv("FTL_ENDPOINT");
        String testEndpoint = System.getProperty("ftl.test.endpoint"); //set by the test framework
        if (testEndpoint != null) {
            endpoint = testEndpoint;
        }
        if (endpoint == null) {
            endpoint = "http://localhost:8892";
        }
        var uri = URI.create(endpoint);
        this.moduleName = System.getProperty("ftl.module.name");
        var channelBuilder = ManagedChannelBuilder.forAddress(uri.getHost(), uri.getPort());
        if (uri.getScheme().equals("http")) {
            channelBuilder.usePlaintext();
        }
        var channel = channelBuilder.build();
        moduleService = ModuleServiceGrpc.newStub(channel);
        moduleService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
        verbService = VerbServiceGrpc.newStub(channel);
    }

    public byte[] getSecret(String secretName) {
        var context = getModuleContext();
        if (context.containsSecrets(secretName)) {
            return context.getSecretsMap().get(secretName).toByteArray();
        }
        throw new RuntimeException("Secret not found: " + secretName);
    }

    public byte[] getConfig(String secretName) {
        var context = getModuleContext();
        if (context.containsConfigs(secretName)) {
            return context.getConfigsMap().get(secretName).toByteArray();
        }
        throw new RuntimeException("Config not found: " + secretName);
    }

    public Datasource getDatasource(String name) {
        //TODO: only one database is supported at the moment
        List databasesList = getModuleContext().getDatabasesList();
        for (var i : databasesList) {
            if (i.getName().equals(name)) {
                return Datasource.fromDSN(i.getDsn());
            }
        }
        return null;
    }

    public byte[] callVerb(String name, String module, byte[] payload) {
        CompletableFuture cf = new CompletableFuture<>();

        verbService.call(CallRequest.newBuilder().setVerb(Ref.newBuilder().setModule(module).setName(name))
                .setBody(ByteString.copyFrom(payload)).build(), new StreamObserver<>() {

                    @Override
                    public void onNext(CallResponse callResponse) {
                        if (callResponse.hasError()) {
                            cf.completeExceptionally(new RuntimeException(callResponse.getError().getMessage()));
                        } else {
                            cf.complete(callResponse.getBody().toByteArray());
                        }
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        cf.completeExceptionally(throwable);
                    }

                    @Override
                    public void onCompleted() {

                    }
                });
        try {
            return cf.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void publishEvent(String topic, String callingVerbName, byte[] event) {
        CompletableFuture cf = new CompletableFuture<>();
        moduleService.publishEvent(PublishEventRequest.newBuilder()
                .setCaller(callingVerbName).setBody(ByteString.copyFrom(event))
                .setTopic(Ref.newBuilder().setModule(moduleName).setName(topic).build()).build(),
                new StreamObserver() {
                    @Override
                    public void onNext(PublishEventResponse publishEventResponse) {
                        cf.complete(null);
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        cf.completeExceptionally(throwable);
                    }

                    @Override
                    public void onCompleted() {
                        cf.complete(null);
                    }
                });
        try {
            cf.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    public LeaseHandle acquireLease(Duration duration, String... keys) throws LeaseFailedException {
        CompletableFuture cf = new CompletableFuture<>();
        var client = moduleService.acquireLease(new StreamObserver() {
            @Override
            public void onNext(AcquireLeaseResponse value) {
                cf.complete(null);
            }

            @Override
            public void onError(Throwable t) {
                cf.completeExceptionally(t);
            }

            @Override
            public void onCompleted() {
                if (!cf.isDone()) {
                    onError(new RuntimeException("stream closed"));
                }
            }
        });
        client.onNext(AcquireLeaseRequest.newBuilder().setModule(moduleName)
                .addAllKey(Arrays.asList(keys))
                .setTtl(com.google.protobuf.Duration.newBuilder()
                        .setSeconds(duration.toSeconds()))
                .build());
        try {
            cf.get();
        } catch (Exception e) {
            throw new LeaseFailedException("lease already held", e);
        }
        return new LeaseHandle() {
            @Override
            public void close() {
                client.onCompleted();
            }
        };
    }

    private ModuleContextResponse getModuleContext() {
        var moduleContext = moduleContextResponse;
        if (moduleContext != null) {
            return moduleContext;
        }
        synchronized (moduleObserver) {
            for (;;) {
                moduleContext = moduleContextResponse;
                if (moduleContext != null) {
                    return moduleContext;
                }
                if (currentError != null) {
                    throw new RuntimeException(currentError);
                }
                waiters = true;
                try {
                    moduleObserver.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }

    private class ModuleObserver implements StreamObserver {

        final AtomicInteger failCount = new AtomicInteger();

        @Override
        public void onNext(ModuleContextResponse moduleContextResponse) {
            synchronized (this) {
                currentError = null;
                FTLController.this.moduleContextResponse = moduleContextResponse;
                if (waiters) {
                    this.notifyAll();
                    waiters = false;
                }
            }

        }

        @Override
        public void onError(Throwable throwable) {
            log.error("GRPC connection error", throwable);
            synchronized (this) {
                currentError = throwable;
                if (waiters) {
                    this.notifyAll();
                    waiters = false;
                }
            }
            if (failCount.incrementAndGet() < 5) {
                moduleService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
            }
        }

        @Override
        public void onCompleted() {
            onError(new RuntimeException("connection closed"));
        }
    }

    public record Datasource(String connectionString, String username, String password) {

        public static Datasource fromDSN(String dsn) {
            try {
                URI uri = new URI(dsn);
                String username = "";
                String password = "";
                String userInfo = uri.getUserInfo();
                if (userInfo != null) {
                    var split = userInfo.split(":");
                    username = split[0];
                    password = split[1];
                    return new Datasource(
                            new URI("jdbc:postgresql", null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), null)
                                    .toASCIIString(),
                            username, password);
                } else {
                    //TODO: this is horrible, just quick hack for now
                    var matcher = Pattern.compile("[&?]user=([^?]*)").matcher(dsn);
                    if (matcher.find()) {
                        username = matcher.group(1);
                    }
                    dsn = matcher.replaceAll("");
                    matcher = Pattern.compile("[&?]password=([^?]*)").matcher(dsn);
                    if (matcher.find()) {
                        password = matcher.group(1);
                    }
                    dsn = matcher.replaceAll("");
                    dsn = dsn.replaceAll("postgresql://", "jdbc:postgresql://");
                    dsn = dsn.replaceAll("postgres://", "jdbc:postgresql://");
                    return new Datasource(dsn, username, password);
                }
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy