/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.wildfly.extension.batch.jberet.deployment;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.batch.operations.JobExecutionAlreadyCompleteException;
import javax.batch.operations.JobExecutionIsRunningException;
import javax.batch.operations.JobExecutionNotMostRecentException;
import javax.batch.operations.JobExecutionNotRunningException;
import javax.batch.operations.JobOperator;
import javax.batch.operations.JobRestartException;
import javax.batch.operations.JobSecurityException;
import javax.batch.operations.JobStartException;
import javax.batch.operations.NoSuchJobException;
import javax.batch.operations.NoSuchJobExecutionException;
import javax.batch.operations.NoSuchJobInstanceException;
import javax.batch.runtime.JobExecution;
import javax.batch.runtime.JobInstance;
import javax.batch.runtime.StepExecution;
import org.jberet.operations.AbstractJobOperator;
import org.jberet.runtime.JobExecutionImpl;
import org.jberet.spi.BatchEnvironment;
import org.jboss.as.server.suspend.ServerActivity;
import org.jboss.as.server.suspend.ServerActivityCallback;
import org.jboss.as.server.suspend.SuspendController;
import org.jboss.msc.inject.Injector;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.wildfly.extension.batch.jberet.BatchConfiguration;
import org.wildfly.extension.batch.jberet._private.BatchLogger;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.manager.WildFlySecurityManager;
/**
* A delegating {@linkplain javax.batch.operations.JobOperator job operator} to interact with the batch environment on
* deployments.
* <p>
* Note that for each method the job name, or derived job name, must exist for the deployment. The allowed job names and
* job XML descriptor are determined at deployment time.
* </p>
* <p>
* This implementation does change some of the API's contracts however it's only intended to be used by management
* resources and operations. Limits the interaction with the jobs to the scope of the deployments jobs. Any behavioral
* change will be documented.
* </p>
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class JobOperatorService extends AbstractJobOperator implements WildFlyJobOperator, JobOperator, Service<JobOperator> {
private static final Properties RESTART_PROPS = new Properties();
private final InjectedValue<BatchConfiguration> batchConfigurationInjector = new InjectedValue<>();
private final InjectedValue<SecurityAwareBatchEnvironment> batchEnvironmentInjector = new InjectedValue<>();
private final InjectedValue<ExecutorService> executorInjector = new InjectedValue<>();
private final InjectedValue<SuspendController> suspendControllerInjector = new InjectedValue<>();
private volatile SecurityAwareBatchEnvironment batchEnvironment;
private volatile ClassLoader classLoader;
private final Boolean restartJobsOnResume;
private final WildFlyJobXmlResolver resolver;
private final BatchJobServerActivity serverActivity;
private final String deploymentName;
private final ThreadLocal<Boolean> permissionsCheckEnabled = ThreadLocal.withInitial(() -> Boolean.TRUE);
public JobOperatorService(final Boolean restartJobsOnResume, final String deploymentName, final WildFlyJobXmlResolver resolver) {
this.restartJobsOnResume = restartJobsOnResume;
this.deploymentName = deploymentName;
this.resolver = resolver;
this.serverActivity = new BatchJobServerActivity();
}
@Override
public void start(final StartContext context) throws StartException {
final BatchEnvironment batchEnvironment = this.batchEnvironment = batchEnvironmentInjector.getValue();
// Get the class loader from the environment
classLoader = batchEnvironment.getClassLoader();
suspendControllerInjector.getValue().registerActivity(serverActivity);
}
@Override
public void stop(final StopContext context) {
// Remove the server activity
suspendControllerInjector.getValue().unRegisterActivity(serverActivity);
final ExecutorService service = executorInjector.getValue();
final Runnable task = () -> {
// Should already be stopped, but just to be safe we'll make one more attempt
serverActivity.stopRunningJobs(false);
batchEnvironment = null;
classLoader = null;
context.complete();
};
try {
service.execute(task);
} catch (RejectedExecutionException e) {
task.run();
} finally {
context.asynchronous();
}
}
@Override
public JobOperator getValue() throws IllegalStateException, IllegalArgumentException {
return this;
}
@Override
protected SecurityAwareBatchEnvironment getBatchEnvironment() {
if (batchEnvironment == null) {
throw BatchLogger.LOGGER.jobOperatorServiceStopped();
}
return batchEnvironment;
}
@Override
public Set<String> getJobNames() throws JobSecurityException {
checkState();
return super.getJobNames().stream()
.filter(resolver::isValidJobName)
.collect(Collectors.toSet());
}
@Override
public int getJobInstanceCount(final String jobName) throws NoSuchJobException, JobSecurityException {
checkState(jobName);
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
return super.getJobInstanceCount(jobName);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public List<JobInstance> getJobInstances(final String jobName, final int start, final int count) throws NoSuchJobException, JobSecurityException {
checkState(jobName);
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
return super.getJobInstances(jobName, start, count);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public List<Long> getRunningExecutions(final String jobName) throws NoSuchJobException, JobSecurityException {
checkState(jobName);
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
return super.getRunningExecutions(jobName);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public Properties getParameters(final long executionId) throws NoSuchJobExecutionException, JobSecurityException {
checkState();
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(executionId);
validateJob(instance.getJobName());
return super.getParameters(executionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public long start(final String jobXMLName, final Properties jobParameters) throws JobStartException, JobSecurityException {
checkState(null, "start");
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final String jobXml;
if (jobXMLName.endsWith(".xml")) {
jobXml = jobXMLName;
} else {
jobXml = jobXMLName + ".xml";
}
if (resolver.isValidJobXmlName(jobXml)) {
return super.start(jobXml, jobParameters, getBatchEnvironment().getCurrentUserName());
}
throw BatchLogger.LOGGER.couldNotFindJobXml(jobXMLName);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public long restart(final long executionId, final Properties restartParameters) throws JobExecutionAlreadyCompleteException, NoSuchJobExecutionException, JobExecutionNotMostRecentException, JobRestartException, JobSecurityException {
checkState(null, "restart");
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(executionId);
validateJob(instance.getJobName());
return super.restart(executionId, restartParameters, getBatchEnvironment().getCurrentUserName());
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public void stop(final long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException, JobSecurityException {
checkState(null, "stop");
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(executionId);
validateJob(instance.getJobName());
super.stop(executionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public void abandon(final long executionId) throws NoSuchJobExecutionException, JobExecutionIsRunningException, JobSecurityException {
checkState(null, "abandon");
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(executionId);
validateJob(instance.getJobName());
super.abandon(executionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public JobInstance getJobInstance(final long executionId) throws NoSuchJobExecutionException, JobSecurityException {
checkState();
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(executionId);
validateJob(instance.getJobName());
return super.getJobInstance(executionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public List<JobExecution> getJobExecutions(final JobInstance instance) throws NoSuchJobInstanceException, JobSecurityException {
checkState();
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
validateJob(instance.getJobName());
return super.getJobExecutions(instance);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public JobExecution getJobExecution(final long executionId) throws NoSuchJobExecutionException, JobSecurityException {
checkState();
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = getJobInstance(executionId);
validateJob(instance.getJobName());
return super.getJobExecution(executionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public List<StepExecution> getStepExecutions(final long jobExecutionId) throws NoSuchJobExecutionException, JobSecurityException {
checkState();
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final JobInstance instance = super.getJobInstance(jobExecutionId);
validateJob(instance.getJobName());
return super.getStepExecutions(jobExecutionId);
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
}
}
@Override
public Collection<String> getJobXmlNames() {
return resolver.getJobXmlNames(classLoader);
}
@Override
public Collection<String> getJobXmlNames(final String jobName) {
return resolver.getJobXmlNames(jobName);
}
@Override
public Set<String> getAllJobNames() {
return resolver.getJobNames();
}
public InjectedValue<BatchConfiguration> getBatchConfigurationInjector() {
return batchConfigurationInjector;
}
/**
* Set the batch environment to use for setting up the correct class loader for delegating executions.
*
* @return the injector used to inject the value in
*/
public Injector<SecurityAwareBatchEnvironment> getBatchEnvironmentInjector() {
return batchEnvironmentInjector;
}
public Injector<ExecutorService> getExecutorServiceInjector() {
return executorInjector;
}
public InjectedValue<SuspendController> getSuspendControllerInjector() {
return suspendControllerInjector;
}
private void checkState() {
checkState(null);
}
private void checkState(final String jobName) {
checkState(jobName, "read");
}
private void checkState(final String jobName, final String targetName) {
if (batchEnvironment == null || classLoader == null) {
throw BatchLogger.LOGGER.jobOperatorServiceStopped();
}
checkPermission(targetName);
if (jobName != null) {
validateJob(jobName);
}
}
private void checkPermission(final String targetName) {
if (permissionsCheckEnabled.get()) {
final SecurityAwareBatchEnvironment environment = getBatchEnvironment();
final SecurityIdentity identity = environment.getIdentity();
if (identity != null) {
final BatchPermission permission = BatchPermission.forName(targetName);
if (!identity.implies(permission)) {
throw BatchLogger.LOGGER.unauthorized(identity.getPrincipal().getName(), permission);
}
}
}
}
private synchronized void validateJob(final String name) {
// In JBeret 1.2.x null means all jobs, in JBeret 1.3.x+ * means all jobs if the name is null or * then ignore
// the check
if (name == null || "*".equals(name)) return;
// Check that this is a valid job name
if (!resolver.isValidJobName(name)) {
throw BatchLogger.LOGGER.noSuchJobException(name);
}
}
private class BatchJobServerActivity implements ServerActivity {
private final AtomicBoolean jobsStopped = new AtomicBoolean(false);
private final AtomicBoolean jobsRestarted = new AtomicBoolean(false);
private final Collection<Long> stoppedIds = Collections.synchronizedCollection(new ArrayList<>());
@Override
public void preSuspend(final ServerActivityCallback serverActivityCallback) {
serverActivityCallback.done();
}
@Override
public void suspended(final ServerActivityCallback serverActivityCallback) {
try {
stopRunningJobs(isRestartOnResume());
} finally {
serverActivityCallback.done();
}
}
@Override
public void resume() {
restartStoppedJobs();
}
private void stopRunningJobs(final boolean queueForRestart) {
if (jobsStopped.compareAndSet(false, true)) {
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
permissionsCheckEnabled.set(Boolean.FALSE);
try {
// Use the deployment's class loader to stop jobs
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final Collection<String> jobNames = getJobNames();
// Look for running jobs and attempt to stop each one
for (String jobName : jobNames) {
// Casting to (Supplier<List<Long>>) is done here on purpose as a workaround for a bug in 1.8.0_45
final List<Long> runningJobs = allowMissingJob((Supplier<List<Long>>) () -> getRunningExecutions(jobName), Collections.emptyList());
for (Long id : runningJobs) {
try {
BatchLogger.LOGGER.stoppingJob(id, jobName, deploymentName);
// We want to skip the permissions check, we need to stop jobs regardless of the
// permissions
stop(id);
// Queue for a restart on resume if required
if (queueForRestart) {
stoppedIds.add(id);
}
} catch (Exception e) {
BatchLogger.LOGGER.stoppingJobFailed(e, id, jobName, deploymentName);
}
}
}
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
// Reset the stopped state
jobsStopped.set(false);
permissionsCheckEnabled.set(Boolean.TRUE);
}
}
}
private void restartStoppedJobs() {
if (isRestartOnResume() && jobsRestarted.compareAndSet(false, true)) {
final ClassLoader current = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
try {
// Use the deployment's class loader to stop jobs
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader);
final Collection<Long> ids = new ArrayList<>();
synchronized (stoppedIds) {
ids.addAll(stoppedIds);
stoppedIds.clear();
}
for (Long id : ids) {
String jobName = null;
String user = null;
try {
final JobExecutionImpl execution = getJobExecutionImpl(id);
jobName = execution.getJobName();
user = execution.getUser();
} catch (Exception ignore) {
}
try {
final long newId;
// If the user is not null we need to restart the job with the user specified
if (user == null) {
newId = restart(id, RESTART_PROPS);
} else {
newId = privilegedRunAs(user, () -> restart(id, RESTART_PROPS));
}
BatchLogger.LOGGER.restartingJob(jobName, id, newId);
} catch (Exception e) {
BatchLogger.LOGGER.failedRestartingJob(e, id, jobName, deploymentName);
}
}
} finally {
WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(current);
// Reset the restart state
jobsRestarted.set(false);
}
}
}
private <V> V privilegedRunAs(final String user, final Callable<V> callable) throws Exception {
final SecurityDomain securityDomain = getBatchEnvironment().getSecurityDomain();
if (securityDomain == null) {
return callable.call();
}
final SecurityIdentity securityIdentity;
if (user != null) {
if (WildFlySecurityManager.isChecking()) {
securityIdentity = AccessController.doPrivileged((PrivilegedAction<SecurityIdentity>) () -> securityDomain.getAnonymousSecurityIdentity().createRunAsIdentity(user, false));
} else {
securityIdentity = securityDomain.getAnonymousSecurityIdentity().createRunAsIdentity(user, false);
}
} else {
securityIdentity = securityDomain.getCurrentSecurityIdentity();
}
return securityIdentity.runAs(callable);
}
private boolean isRestartOnResume() {
if (restartJobsOnResume == null) {
return batchConfigurationInjector.getValue().isRestartOnResume();
}
return restartJobsOnResume;
}
}
}