Skip to content

Commit

Permalink
cluster formation DSL - Gradle integration - part 2 (#32028)
Browse files Browse the repository at this point in the history
* Implement Version in java

- This allows to move all  all .java files from .groovy.
- Will prevent eclipse from tangling up in this setup
- make it possible to use Version from Java

* PR review comments

* Cluster formation plugin with reference counting

```
> Task :plugins:ingest-user-agent:listElasticSearchClusters
Starting cluster: myTestCluster
   * myTestCluster: /home/alpar/work/elastic/elasticsearch/plugins/ingest-user-agent/foo
Asked to unClaimAndStop myTestCluster, since cluster still has 1 claim it will not be stopped

> Task :plugins:ingest-user-agent:testme UP-TO-DATE
Stopping myTestCluster, since no of claims is 0
```

- Meant to auto manage the clusters lifecycle
- Add integration test for cluster formation

* Fix rebase

* Change to `useCluster` method on task
  • Loading branch information
alpar-t committed Aug 15, 2018
1 parent 068d03f commit e680a64
Show file tree
Hide file tree
Showing 10 changed files with 674 additions and 0 deletions.
9 changes: 9 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ plugins {
id 'groovy'
}

gradlePlugin {
plugins {
simplePlugin {
id = 'elasticsearch.clusterformation'
implementationClass = 'org.elasticsearch.gradle.clusterformation.ClusterformationPlugin'
}
}
}

group = 'org.elasticsearch.gradle'

String minimumGradleVersion = file('src/main/resources/minimumGradleVersion').text.trim()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch;

import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileTree;
import org.gradle.api.tasks.WorkResult;
import org.gradle.process.ExecResult;
import org.gradle.process.JavaExecSpec;

import java.io.File;

/**
* Facilitate access to Gradle services without a direct dependency on Project.
*
* In a future release Gradle will offer service injection, this adapter plays that role until that time.
* It exposes the service methods that are part of the public API as the classes implementing them are not.
* Today service injection is <a href="https://github.com/gradle/gradle/issues/2363">not available</a> for
* extensions.
*
* Everything exposed here must be thread safe. That is the very reason why project is not passed in directly.
*/
public class GradleServicesAdapter {

public final Project project;

public GradleServicesAdapter(Project project) {
this.project = project;
}

public static GradleServicesAdapter getInstance(Project project) {
return new GradleServicesAdapter(project);
}

public WorkResult copy(Action<? super CopySpec> action) {
return project.copy(action);
}

public WorkResult sync(Action<? super CopySpec> action) {
return project.sync(action);
}

public ExecResult javaexec(Action<? super JavaExecSpec> action) {
return project.javaexec(action);
}

public FileTree zipTree(File zipPath) {
return project.zipTree(zipPath);
}
}
36 changes: 36 additions & 0 deletions buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.gradle;

public enum Distribution {

INTEG_TEST("integ-test-zip"),
ZIP("zip"),
ZIP_OSS("zip-oss");

private final String name;

Distribution(String name) {
this.name = name;
}

public String getName() {
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.gradle.clusterformation;

import org.gradle.api.Task;
import org.gradle.api.execution.TaskActionListener;
import org.gradle.api.execution.TaskExecutionListener;
import org.gradle.api.tasks.TaskState;

public class ClusterFormationTaskExecutionListener implements TaskExecutionListener, TaskActionListener {
@Override
public void afterExecute(Task task, TaskState state) {
// always unclaim the cluster, even if _this_ task is up-to-date, as others might not have been and caused the
// cluster to start.
ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(ElasticsearchConfiguration::unClaimAndStop);
}

@Override
public void beforeActions(Task task) {
// we only start the cluster before the actions, so we'll not start it if the task is up-to-date
ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(ElasticsearchConfiguration::start);
}

@Override
public void beforeExecute(Task task) {
}

@Override
public void afterActions(Task task) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.gradle.clusterformation;

import org.gradle.api.Task;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ClusterFormationTaskExtension {

private final Task task;

private final List<ElasticsearchConfiguration> claimedClusters = new ArrayList<>();

private final Logger logger = Logging.getLogger(ClusterFormationTaskExtension.class);

public ClusterFormationTaskExtension(Task task) {
this.task = task;
}

public void call(ElasticsearchConfiguration cluster) {
// not likely to configure the same task from multiple threads as of Gradle 4.7, but it's the right thing to do
synchronized (claimedClusters) {
if (claimedClusters.contains(cluster)) {
logger.warn("{} already claimed cluster {} will not claim it again",
task.getPath(), cluster.getName()
);
return;
}
claimedClusters.add(cluster);
}
logger.info("CF: the {} task will use cluster: {}", task.getName(), cluster.getName());
}

public List<ElasticsearchConfiguration> getClaimedClusters() {
synchronized (claimedClusters) {
return Collections.unmodifiableList(claimedClusters);
}
}

static ClusterFormationTaskExtension getForTask(Task task) {
return task.getExtensions().getByType(ClusterFormationTaskExtension.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.gradle.clusterformation;

import groovy.lang.Closure;
import org.elasticsearch.GradleServicesAdapter;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ExtraPropertiesExtension;

public class ClusterformationPlugin implements Plugin<Project> {

public static final String LIST_TASK_NAME = "listElasticSearchClusters";
public static final String EXTENSION_NAME = "elasticSearchClusters";
public static final String TASK_EXTENSION_NAME = "useClusterExt";

private final Logger logger = Logging.getLogger(ClusterformationPlugin.class);

@Override
public void apply(Project project) {
NamedDomainObjectContainer<? extends ElasticsearchConfiguration> container = project.container(
ElasticsearchNode.class,
(name) -> new ElasticsearchNode(name, GradleServicesAdapter.getInstance(project))
);
project.getExtensions().add(EXTENSION_NAME, container);

Task listTask = project.getTasks().create(LIST_TASK_NAME);
listTask.setGroup("ES cluster formation");
listTask.setDescription("Lists all ES clusters configured for this project");
listTask.doLast((Task task) ->
container.forEach((ElasticsearchConfiguration cluster) ->
logger.lifecycle(" * {}: {}", cluster.getName(), cluster.getDistribution())
)
);

// register an extension for all current and future tasks, so that any task can declare that it wants to use a
// specific cluster.
project.getTasks().all((Task task) -> {
ClusterFormationTaskExtension taskExtension = task.getExtensions().create(
TASK_EXTENSION_NAME, ClusterFormationTaskExtension.class, task
);
// Gradle doesn't look for extensions that might implement `call` and instead only looks for task methods
// work around by creating a closure in the extra properties extensions which does work
task.getExtensions().findByType(ExtraPropertiesExtension.class)
.set(
"useCluster",
new Closure<Void>(this, this) {
public void doCall(ElasticsearchConfiguration conf) {
taskExtension.call(conf);
}
}
);
});

// Make sure we only claim the clusters for the tasks that will actually execute
project.getGradle().getTaskGraph().whenReady(taskExecutionGraph ->
taskExecutionGraph.getAllTasks().forEach(
task -> ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(
ElasticsearchConfiguration::claim
)
)
);

// create the listener to start the clusters on-demand and terminate when no longer claimed.
// we need to use a task execution listener, as tasl
project.getGradle().addListener(new ClusterFormationTaskExecutionListener());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.gradle.clusterformation;

import org.elasticsearch.gradle.Distribution;
import org.elasticsearch.gradle.Version;

import java.util.concurrent.Future;

public interface ElasticsearchConfiguration {
String getName();

Version getVersion();

void setVersion(Version version);

default void setVersion(String version) {
setVersion(Version.fromString(version));
}

Distribution getDistribution();

void setDistribution(Distribution distribution);

void claim();

Future<Void> start();

void unClaimAndStop();
}
Loading

0 comments on commit e680a64

Please sign in to comment.