How to restart a 'run' in continuous mode


(Staffan Olsson) #1

I initially asked this question in https://stackoverflow.com/questions/44918632/can-gradle-restart-running-app-in-continuous-build-mode-run.

We have continuous mode working with -d, but only when I let my main in run exit. Is there any way to terminate a running app on change?

We’ve found such behaviour very useful with Nodemon when developing web services. For example to test the service with dependencies we run a container in a docker-compose setup.


(uklance) #2

I’d guess your run task should depend on a maybeStop task which stops the app if it’s running (connects to a port, executes a restful stop call, places a stop file in a known location etc).


(Staffan Olsson) #3

I don’t think I get what you mean. The only resource I can find on maybeStop is some source in https://github.com/gradle/gradle/blob/master/subprojects/messaging/src/main/java/org/gradle/internal/remote/internal/hub/ConnectionSet.java.

Are you saying that an arbitrary task that run depends on will be executed on change, even if run is still live?


(uklance) #4

Your application will need to start in a separate process to the gradle process so that gradle can finalise / exit after starting it. If you’re “holding up” the gradle thread whilst your application is running then continuous mode won’t help you.

The maybeStop task doesn’t exist yet, this will be a custom task created by yourself with your own hand rolled stop functionality (eg REST call, socket connect, MBean invocation etc etc)

I’m sure gretty solves this problem, perhaps a poke through the gretty sources can help


(Staffan Olsson) #5

In the original post I wrote -d but I meant -t.

Are you sure Gradle is held up? I posted a small sample app at https://github.com/solsson/gradlemon and from the debug output it looks like changes are successfully picked up in the background, in between the output from the running app.


(uklance) #6

Gradle has a pool of worker threads so you’re likely holding onto one with your application and another is picking up changes


(Sterling Greene) #7

There’s not a public API for this, but we have something like this with Play. We need to make this a public feature: https://github.com/gradle/gradle/issues/2336 Would you be interested in helping out with this?

Gretty re-implements a continuous build-like feature itself.


(Staffan Olsson) #8

Thanks for the pointers. I just started using Gradle, with the mission to introduce Java as a complement to our Node.js based services. I had a brief look at the sources for the internal APIs in #2336 and for filewatch.jdk7, and I guess I have a bit of a learning curve. I will fork and give it a try over the coming weeks, at least to understand how a restart can be triggered.


(Sterling Greene) #9

This is probably the shortest example of what it takes to implement your own DeploymentHandle to work with continuous build:

https://github.com/gradle/gradle/issues/2336 is basically needed to expose this as a public thing, so you’re not relying on internal APIs and it’s a little more discoverable/feature complete.


(Luke Daley) #10

Here’s something we use internally for integrating the Gradle Node plugin with continuous build:

import com.moowork.gradle.node.exec.NodeExecRunner
import org.gradle.api.invocation.Gradle
import org.gradle.api.tasks.TaskAction
import org.gradle.deployment.internal.DeploymentHandle
import org.gradle.deployment.internal.DeploymentRegistry
import org.gradle.process.ExecSpec

class ContinuousNodeScript extends AbstractNodeScript {

    List<String> args = []

    Runnable onStop = {
        // nothing
    }

    @TaskAction
    void start() {
        if (project.gradle.startParameter.continuous) {
            def deploymentRegistry = services.get(DeploymentRegistry)
            def deploymentHandle = deploymentRegistry.get(Handle, getPath())
            if (deploymentHandle == null) {
                deploymentRegistry.register(getPath(), new Handle(runner(), args, onStop))
            }
        } else {
            run(runner(), args)
        }
    }

    private static run(NodeExecRunner runner, List<String> args) {
        runner.setArguments(args)
        runner.execOverrides = { ExecSpec execSpec ->
            execSpec.errorOutput = System.err
            execSpec.standardOutput = System.out
            execSpec.ignoreExitValue = true
        }
        runner.execute()
    }

    private static class Handle implements DeploymentHandle {

        private final Runnable onStop
        boolean stopped

        Handle(NodeExecRunner runner, List<String> args, Runnable onStop) {
            this.onStop = onStop
            Thread.start { run(runner, args) }

            // Gradle won't shut down deployments on SIGINT
            // Under some circumstances, the child process could detach
            Runtime.runtime.addShutdownHook {
                onStop.run()
            }
        }

        @Override
        boolean isRunning() {
            !stopped
        }

        @Override
        void onNewBuild(Gradle gradle) {

        }

        @Override
        void stop() {
            onStop.run()
            stopped = true
        }
    }
}
import com.moowork.gradle.node.NodeExtension
import com.moowork.gradle.node.exec.NodeExecRunner
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity

abstract class AbstractNodeScript extends DefaultTask {

    protected NodeExecRunner runner() {
        new NodeExecRunner(project)
    }

    String script(String path) {
        new File(nodeModulesDir(), path).absolutePath
    }

    @Internal
    protected File nodeModulesDir() {
        new File(config.nodeModulesDir, "node_modules")
    }

    @InputFile
    @Optional
    @PathSensitive(PathSensitivity.RELATIVE)
    protected getPackageJsonFile() {
        return new File(config.nodeModulesDir, 'package.json')
    }

    @InputFile
    @Optional
    @PathSensitive(PathSensitivity.RELATIVE)
    protected getYarnLockFile() {
        return new File(config.nodeModulesDir, 'yarn.lock')
    }

    protected NodeExtension getConfig() {
        return this.project.extensions.getByType(NodeExtension)
    }
}

That might give you a start.


(Staffan Olsson) #11

An update… I’ve concluded that even with this in place we would probably not develop Java services the way we do with Node.js. Instead we’d use gradle to generate workspaces for Eclipse/IntelliJ and make projects self-contained in terms of testing.

Docker Compose is invaluable for testing our Node.js services, which means that we want fast roundtrips on source change inside a container.

In java we’d do like pre-docker: where mocking does not suffice we’ll run 3rd party dependencies (Kafka, RDBMS) in the junit setup. This is a lot easier with Java than with Node, because it’s been done like this for (relative) ages.

We’ll use Gradle for CI/CD. All our build jobs are already encapsulated using Docker, and Gradle is compatible with that already.