Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fork a Maven lifecycle (in the proper sense) from a plugin?

General Problem: I'm testing a web application at a large company with a service oriented architecture. External services often fail in our test environment due to background noise. This prevents integration tests for our service from running properly since our service won't work unless calls to these external services are succeeding. For this reason we'd like the ability to mock responses from external services so that we don't have to depend on them and can test our own service in isolation.

There's a tool for this called Mockey which we are hoping to use. It's a Java program that runs through an embedded Jetty server and acts as a proxy for service calls. Our web application is re-configured to call Mockey instead of the external services. Mockey is then configured to provide dynamically mocked responses to these calls depending on the URL and header data that get passed in.

In order to utilize this tool we'd like the ability to start Mockey during the pre-integration-test phase of a Maven lifecycle so that it will be available for use during the integration-test phase.

Specific Problem: In order to start and shutdown Mockey during the pre-integration-test and post-integration-test phases of the Maven lifecycle I've written a Maven 3 plugin called mockey-maven-plugin:

The mockey-maven-plugin pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mycompany.mockey</groupId>
    <artifactId>mockey-maven-plugin</artifactId>
    <packaging>maven-plugin</packaging>
    <version>1.3</version>

    <dependencies>

        <!-- Maven plugin dependencies -->
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
            <scope>provided</scope>
        </dependency>

        <!-- Mockey dependency -->
        <dependency>
            <groupId>com.mycompany.mockey</groupId>
            <artifactId>Mockey</artifactId>
            <version>1.16.2015</version>
        </dependency>

    </dependencies>

    <build>

        <plugins>

            <!-- This plugin is used to generate a plugin descriptor
                 xml file which will be packaged with the plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.4</version>
            </plugin>

        </plugins>

    </build>

</project>

The mockey-maven-plugin StartMockey class:

@Mojo(name="start-mockey")
@Execute(phase= LifecyclePhase.PACKAGE) // Not sure about this annotation
public class StartMockey extends AbstractMojo
{
    /**
     * Flag which controls Mockey startup.
     */
    @Parameter(property="mockey.skipStartup", defaultValue="false", required=true)
    private Boolean skipStartup;

    // Do I need these getters and setters or does Maven ignore them?

    public Boolean getSkipStartup()
    {
        return skipStartup;
    }

    public void setSkipStartup(Boolean skipStartup)
    {
        this.skipStartup = skipStartup;
    }

    // *SNIP* Defining Mockey parameters...

    // Maven will call this method to start the mockey-maven-plugin
    public void execute()
    {
        if(skipStartup)
        {
            getLog().info("Skipping Mockey startup");
            return;
        }

        getLog().info("Starting Mockey");

        // Load specified parameters into array
        List<String> argsList = new ArrayList<>();

        // *SNIP* Adding Mockey parameters to argList...

        String[] args = new String[argsList.size()];
        argsList.toArray(args);

        // Start Mockey with specified parameters and wait for it to return
        try
        {
            JettyRunner.main(args);
        }
        catch(Exception e)
        {
            getLog().error("Mockey died... :(");
        }
        getLog().info("mockey-maven-plugin now exiting");
    }
}

The mockey-maven-plugin ShutdownMockey class:

@Mojo(name="shutdown-mockey")
public class ShutdownMockey extends AbstractMojo
{
    /**
     * Flag which controls Mockey shutdown.
     */
    @Parameter(property="mockey.skipShutdown")
    private Boolean skipShutdown;

    // Again, Do I need these getters and setters or does Maven ignore them?

    public Boolean getSkipShutdown()
    {
        return skipShutdown;
    }

    public void setSkipShutdown(Boolean skipShutdown)
    {
        this.skipShutdown = skipShutdown;
    }

    public void execute()
    {
        if(skipShutdown)
        {
            getLog().info("Skipping Mockey shutdown");
            return;
        }
        getLog().info("Shutting down Mockey");
        JettyRunner.stopServer();
        getLog().info("mockey-maven-plugin now exiting");
    }
}

Plugin entry for mockey-maven-plugin in my team project's pom.xml file:

<plugin>
    <groupId>com.mycompany.mockey</groupId>
    <artifactId>mockey-maven-plugin</artifactId>
    <version>1.3</version>
    <configuration>
        <skipShutdown>${keepMockeyRunning}</skipShutdown>
        <skipStartup>${skipMockey}</skipStartup>

        <!-- *SNIP* Other Mockey parameters... -->

    </configuration>
    <executions>
        <execution>
            <id>start-mockey</id>
            <goals>
                <goal>start-mockey</goal>
            </goals>
            <phase>pre-integration-test</phase>
        </execution>
        <execution>
            <id>shutdown-mockey</id>
            <goals>
                <goal>shutdown-mockey</goal>
            </goals>
            <phase>post-integration-test</phase>
        </execution>
    </executions>
</plugin>

This plugin works fine for starting Mockey in the pre-integration-test phase, but blocks the build until Mockey has exited. I'm not sure why this is occurring since I added this annotation specifically to prevent that issue:

 @Execute(phase= LifecyclePhase.PACKAGE)

I actually copied this annotation from another plugin which does exactly what I'm trying to do here (We use the maven-tomcat7-plugin for launching our web application locally in the pre-integration-test phase and shutting it down in the post-integration-test phase). I thought this would work the same way, but I'm seeing a different behavior.

Here's what I want to see happen:

  1. Maven build begins on a single thread.
  2. This thread runs through all of the lifecycle phases from validate to package (reference) and executes all of the plugins with goals that are bound to those phases.
  3. The thread gets to the pre-integration-test phase, sees that the mockey-maven-plugin's start-mockey goal is bound to the pre-integration-test phase, and attempts to execute the start-mockey goal.
  4. The start-mockey goal is annotated to execute on a second thread (not the first thread) without running any other goals for any other lifecycle phases beforehand or afterwards on the new thread. The second thread starts Mockey's Jetty server by calling JettyRunner.main(args) and blocks on that method for the time being (it's running Mockey).
  5. The first thread continues on to other goals and phases (IE: run integration tests).
  6. The first thread gets to the post-integration test phase, sees that the mockey-maven-plugin's shutdown-mockey goal is bound to the post-integration-test phase, and executes the shutdown-mockey goal.
  7. The shutdown-mockey goal calls JettyRunner.stopServer() which hooks into a static object inside the JettyRunner class and signals the first thread to shutdown Jetty. Meanwhile the first thread waits for a signal from the second thread (or maybe it's polling, I don't really know) that Jetty has shut down.
  8. The second thread finishes shutting down Jetty, signals to the first thread that it can continue, and kills itself.
  9. The first thread continues on to any additional goals and Maven lifecycle phases.

Here's what I'm actually seeing happen:

  1. Maven build begins on a single thread.
  2. This thread runs through all of the lifecycle phases from validate to package (reference) and executes all of the plugins with goals that are bound to those phases.
  3. The thread gets to the pre-integration-test phase, sees that the mockey-maven-plugin's start-mockey goal is bound to the pre-integration-test phase, and attempts to execute the start-mockey goal.
  4. The start-mockey goal is annotated to execute on a second thread. The second thread starts the entire Maven lifecycle over again beginning in the validate phase.
  5. The first thread blocks while waiting for the second thread to exit.
  6. The second thread runs all the way through the package phase and then kills itself.
  7. The first thread is unblocked and picks up where it left off. It executes the start-mockey goal on its own (never run by the second thread). This calls JettyRunner.main(args) and the thread then blocks while running Mockey's Jetty server.
  8. The thread remains blocked until the Jetty server is manually killed (along with the rest of the Maven lifecycle).

This confuses me primarily because Maven seems to have a different concept of forking than what I'm familiar with. To me forking means to diverge at a particular point, not to start over, and not to affect the original process. When we fork a process in Unix it copies the stack and function pointer of the first process. It does not start over from the beginning of the program. Similarly, when we fork a code repository we start with all of the files and directories that are currently in the original repository. We don't start over with a blank slate. So why, when we "fork" a Maven lifecycle does it abandon everything, start over, and block the original thread? That doesn't seem at all like forking to me. Here's some of the documentation I've read that describes "forking" in Maven:

  • "Running the ZipForkMojo will fork the lifecycle"
  • "Any plugin that declares @execute [phase] will cause the build to fork"
  • "goal=goal to fork... lifecycle=lifecycle id to fork... phase=lifecycle phase to fork..."

Remaining Questions:

  • How can I get Maven to fork in the sense that I'm familiar with?
  • What does it mean to fork a Maven lifecycle to a phase that takes place before the one that you're forking from? For instance, what does it mean to fork to the package phase from the pre-integration-test phase?
  • Why do you think the Tomcat7 plugin is doing this (forking to the package phase from the pre-integration test phase)?
  • What could be different about the Tomcat7 plugin that causes the same annotation to behave differently in my plugin?

Answered Question (see below): - Is there another phase which I should specify in the annotation for my plugin to get it to behave as desired or should I be using the execute annotation in a fundamentally different manner?

like image 937
Alex Jansen Avatar asked Jan 17 '15 00:01

Alex Jansen


1 Answers

See https://books.sonatype.com/mvnref-book/reference/writing-plugins-sect-plugins-lifecycle.html

The docs seem to indicate that you should create a custom lifecycle that only includes the start-mockey goal. Then your @Execute annotation should specify the goal and the lifecycle. That should fork off the execution but only execute your start-mockey. I think then you can run the end-mockey as normal.

like image 85
cjnygard Avatar answered Sep 22 '22 18:09

cjnygard