
oracle.kv.impl.admin.plan.AbstractPlan Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of oracle-nosql-server Show documentation
Show all versions of oracle-nosql-server Show documentation
NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.
The newest version!
/*-
* Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle NoSQL
* Database made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle NoSQL Database for a copy of the license and
* additional information.
*/
package oracle.kv.impl.admin.plan;
import java.io.EOFException;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import oracle.kv.impl.admin.Admin;
import oracle.kv.impl.admin.AdminServiceParams;
import oracle.kv.impl.admin.CommandResult;
import oracle.kv.impl.admin.IllegalCommandException;
import oracle.kv.impl.admin.PlanLocksHeldException;
import oracle.kv.impl.admin.PlanWaiter;
import oracle.kv.impl.admin.plan.ExecutionState.ExceptionTransfer;
import oracle.kv.impl.admin.plan.task.ParallelBundle;
import oracle.kv.impl.admin.plan.task.Task;
import oracle.kv.impl.admin.plan.task.TaskList;
import oracle.kv.impl.metadata.Metadata;
import oracle.kv.impl.security.ResourceOwner;
import oracle.kv.impl.security.login.LoginManager;
import oracle.kv.impl.security.util.SecurityUtils;
import oracle.kv.impl.util.FormatUtils;
import oracle.kv.impl.util.JsonUtils;
import oracle.kv.util.ErrorMessage;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import org.codehaus.jackson.node.ObjectNode;
/**
* Encapsulates a definition and mechanism for making a change to the KV
* Store. Subclasses of AbstractPlan will define the different types of
* AbstractPlan that can be carried out.
*
* Synchronization
* ---------------
* Any modifications to the plan at execution time, including the execution
* state, plan run, and task run instances contained within it, must
* synchronize on the plan instance. At execution time, there will be multiple
* thread modifying these state fields, as well as threads that are trying
* to save the instance to its DPL database.
*
* The synchronization locking order is
* 1. the Admin service (oracle.kv.impl.admin.Admin)
* 2. the plan instance
* 3. JE locks in the AdminDB
*
* Because of that, any synchronized plan methods must be careful not to try to
* acquire the Admin mutex after already owning the plan mutex, lest a deadlock
* occur. [#22161]
*
* Likewise, the mutex on the plan instance must be taken before any JE locks
* are acquired. [#23134] Creating a hierarchy between database locks and
* mutexes is regrettable, but is needed to prevent modification of the plan
* instance while it is being serialized as a precursor to the database
* operation. For example, the plan mutex is taken by the persist() method, in
* this way:
* - obtain plan mutex
* - DPL put (serialize plan, acquire write lock on plan record)
*
* or the DeployTopoPlan.incrementEndCount()
* - obtain plan mutex
* - read current topology from Admin DB (acquire a topo read lock, then
* release)
* - update field in plan based on current topology
*
* Because of this, we must refrain from starting Admin transactions that
* acquire JE locks and then attempt to obtain the plan mutex.
*
* version 0: original
* version 1: removed AdminEntity from inheritance chain
*/
@Entity(version=1)
public abstract class AbstractPlan implements Plan, Serializable {
private static final long serialVersionUID = 1L;
/* Version for plans created before R3.1.0 */
private static final int INITIAL_PLAN_VERSION = 1;
private static final int CURRENT_PLAN_VERSION = 2;
/**
* Plan name prefix for plans generated by the Admin (vs. plans created
* as a result of external actions).
*/
private static final String SYS_PLAN_PREFIX = "SYS$";
/**
* A unique sequence id for a plan.
*/
@PrimaryKey
private int id;
/**
* A user defined name.
*/
private String name;
/**
* The time this plan was created.
*/
protected long createTime;
/**
* A list of tasks that will be executed to carry out this plan.
*/
protected TaskList taskList;
/**
* Plans may be executed multiple times. ExecutionState tracks the status
* of each run.
*/
private ExecutionState executionState;
/**
* A transient reference to the owning Planner. This field must be set
* whenever a Plan is constructed or retrieved from the database.
*/
protected transient Planner planner;
/**
* The plan execution listeners enable monitoring, testing, and
* asynchronous plan execution. Their invocation must be ordered, because
* the optional PlanWaiter signifies that all plan related execution is
* done, and it must be the last callback executed. Listeners may be added
* and viewed concurrently, so access to the list should be synchronized.
*/
private transient LinkedList listeners;
/* Set only when the plan is run. */
protected transient Logger logger;
/**
* From R3.Q3, as a part of authorization, each plan object will be
* associated with its creator as the owner. If a plan is created in an
* earlier version, or is created in a store without security, the owner
* will be null. We make it transient to be compatible with nodes using DPL
* storage during upgrade.
*/
private transient ResourceOwner owner;
/*
* Version of this plan object. Make it transient to be compatible with
* nodes using DPL storage during upgrade.
*/
private transient Integer version;
/**
* Base plan object.
*
* @param name
* @param planner
*/
protected AbstractPlan(String name, Planner planner) {
this(name, planner, false);
}
/**
* Base plan object.
*/
protected AbstractPlan(String name, Planner planner, boolean systemPlan) {
version = CURRENT_PLAN_VERSION;
id = planner.getAndIncrementPlanId();
if (name == null) {
this.name = getDefaultName();
} else {
this.name = name;
}
if (this.name.startsWith(SYS_PLAN_PREFIX)) {
throw new IllegalCommandException("Plan names cannot start" +
" with " + SYS_PLAN_PREFIX);
}
if (systemPlan) {
this.name = SYS_PLAN_PREFIX + this.name;
}
createTime = System.currentTimeMillis();
taskList = new TaskList(TaskList.ExecutionStrategy.SERIAL);
initTransientFields();
this.planner = planner;
executionState = new ExecutionState(name);
initWithParams(planner.getAdmin().getParams());
this.owner = SecurityUtils.currentUserAsOwner();
}
/*
* No-arg ctor for use by DPL, for plans instantiated from the database.
*/
AbstractPlan() {
initTransientFields();
}
protected synchronized void initTransientFields() {
listeners = new LinkedList<>();
}
private void initWithParams(AdminServiceParams aServiceParams) {
addListener(new PlanTracker(aServiceParams));
}
@Override
public void initializePlan(Planner planner1,
AdminServiceParams aServiceParams) {
planner = planner1;
initWithParams(aServiceParams);
}
/**
* Loggers are set just before plan execution.
*/
void setLogger(Logger logger1) {
this.logger = logger1;
}
@Override
public int getId() {
return id;
}
synchronized void validateStartOfRun() {
executionState.validateStartOfNewRun(this);
}
synchronized PlanRun startNewRun() {
return executionState.startNewRun();
}
/**
* Returns true if this plan cannot be run while other plans
* are running.
*/
@Override
public abstract boolean isExclusive();
/**
* Gets the current state of this plan, as far as it is known by the
* system. In the event that this Admin has failed and the plan has been
* read from the database, a plan may temporarily be in a RUNNING state
* before moving to INTERRUPTED.
*
* @return the most recently computed state
*/
@Override
public synchronized State getState() {
return executionState.getLatestState();
}
/**
* Note that checking and changing the state must be atomic, and is
* synchronized.
*/
synchronized void requestApproval() {
final State currentState = getState();
if (currentState.approvedAndCanExecute()) {
/* We're just trying to retry a plan, no need to approve again. */
return;
}
executionState.setPlanState(planner, this,
State.APPROVED, "approval requested");
}
/**
* Called whenever a plan is being canceled. Note that checking and
* changing the state must be atomic, and is synchronized.
*/
synchronized void requestCancellation() {
executionState.setPlanState(planner, this, State.CANCELED,
"cancellation requested");
}
/**
* Check if is possible to directly cancel this plan without doing
* interrupt processing, because it has not
* started, or was already canceled.
* @return true if the plan was not started, and has been canceled,
* false if this plan is running, and if steps must be taken to interrupt
* it.
*/
synchronized boolean cancelIfNotStarted() {
/* Already canceled. */
final State state = executionState.getLatestState();
if (state == State.CANCELED) {
return true;
}
if ((state == State.PENDING) ||
(state == State.APPROVED)) {
requestCancellation();
return true;
}
return false;
}
/**
* Change a RUNNING or INTERRUPT_REQUESTED plan to INTERRUPTED.
*/
@Override
public synchronized void markAsInterrupted() {
final State state = executionState.getLatestState();
if (state == State.RUNNING) {
executionState.setPlanState(planner, this,
State.INTERRUPT_REQUESTED,
"plan recovery");
executionState.setPlanState(planner, this, State.INTERRUPTED,
"plan recovery");
} else if (state == State.INTERRUPT_REQUESTED) {
executionState.setPlanState(planner, this, State.INTERRUPTED,
"plan recovery");
}
}
/**
* Be sure to synchronize properly when setting plan state, so other methods
* which check plan state, like addWaiter(), will be correct.
*/
synchronized void setState(PlanRun planRun,
Planner plr,
State newState,
String msg) {
planRun.setState(plr, this, newState, msg);
}
/**
* Set the request flag to start a plan interruption. CALLER must
* synchronize on the Admin first, because the requestInterrupted may
* attempt to save the plan to the database. Doing so requires the Admin
* mutex. Since the lock hierachy is {@literal Admin->Plan}, the caller
* must synchronize on Admin, and then acquire the plan mutex with this
* call.
*/
synchronized void requestInterrupt() {
final PlanRun planRun = executionState.getLatestPlanRun();
/* This plan isn't running */
if (planRun == null) {
return;
}
if (getState() == State.RUNNING) {
executionState.setPlanState
(planner, this, State.INTERRUPT_REQUESTED,
"plan interrupt");
planner.getAdmin().savePlan(this, Admin.CAUSE_INTERRUPT_REQUEST);
}
planRun.requestInterrupt();
}
/**
* @see Plan#addTask
*/
@Override
public synchronized void addTask(Task t) {
if ((t instanceof ParallelBundle) && ((ParallelBundle)t).isEmpty()) {
return;
}
taskList.add(t);
}
/**
* @return the TaskList for this plan.
*/
@Override
public TaskList getTaskList() {
return taskList;
}
@Override
public String getName() {
return name;
}
@Override
public String toString() {
return "Plan " + id + " [" + getName() + "]";
}
/**
* @return the createTime
*/
@Override
public Date getCreateTime() {
return new Date(createTime);
}
/**
* @return the startTime
*/
@Override
public Date getStartTime() {
return executionState.getLatestStartTime();
}
/**
* @return the endTime
*/
@Override
public Date getEndTime() {
return executionState.getLatestEndTime();
}
@Override
public Planner getPlanner() {
return planner;
}
/**
* Each plan must save any resulting params or topology to the admin at the
* beginning and end of execution.
*/
abstract void preExecutionSave();
public Logger getLogger() {
return logger;
}
@Override
public void stripForDisplay() {
/* Default implementation is a noop */
}
/**
* Return the Admin to which this planner belongs.
*/
public Admin getAdmin() {
if (planner != null) {
return planner.getAdmin();
}
return null;
}
/**
* ExecutionListeners are used to generate monitoring information about
* plan execution, and for testing.
*/
synchronized void addListener(ExecutionListener listener) {
listeners.addFirst(listener);
}
/**
* Listeners must be called in a specific order.
*/
synchronized List getListeners() {
/* Return a new list, to guard against concurrent modification. */
return new ArrayList<>(listeners);
}
/**
* A PlanWaiter should be added at the end of the listener list, so it
* executes after all the other listeners, because it signifies the end of
* all plan related execution.
*/
@Override
public synchronized PlanRun addWaiter(PlanWaiter waiter) {
listeners.addLast(waiter);
if (logger != null) {
logger.log(Level.FINE,
"Adding plan waiter to {0}/{1}, state={2}",
new Object[]{getId(), getName(), getState()});
}
/* If the plan has ended already, release the waiter. */
if (getState().planExecutionFinished()) {
waiter.planEnd(this);
}
return getExecutionState().getLatestPlanRun();
}
@Override
public synchronized void removeWaiter(PlanWaiter waiter) {
listeners.remove(waiter);
}
/**
* @return any failures from the most recent run, for display.
*/
@Override
public String getLatestRunFailureDescription() {
return executionState.getLatestRunFailureDescription();
}
/**
* For unit test support. Get the saved Exception and description for a
* plan failure.
*/
ExceptionTransfer getExceptionTransfer() {
return executionState.getLatestExceptionTransfer();
}
/**
* Return a formatted string representing the history of execution attempts
* for this plan.
*/
@Override
public String showRuns() {
return executionState.showRuns();
}
/**
* Return the execution state object, from which we can extract detailed
* information about the plan's execution history.
*/
@Override
public ExecutionState getExecutionState() {
return executionState;
}
/**
* Return the total number of tasks in the plan, including nested tasks.
*/
@Override
public int getTotalTaskCount() {
return taskList.getTotalTaskCount();
}
/**
* Return true if an interrupt has been requested. Long running tasks
* must check this and return promptly if it's set.
*/
public synchronized boolean isInterruptRequested() {
return executionState.getLatestPlanRun().isInterruptRequested();
}
/**
* Return true if an interrupt has been requested after the cleanup
* phase started, and therefore we should actually interrupt task cleanup.
*/
public synchronized boolean cleanupInterrupted() {
return executionState.getLatestPlanRun().cleanupInterrupted();
}
synchronized void setCleanupStarted() {
executionState.getLatestPlanRun().setCleanupStarted();
}
/**
* Describe all finished tasks, for a status report. Plans can override
* this to provide a more informative, user friendly report for specific
* plans.
*/
@Override
public void describeFinished(final Formatter fm,
final List finished,
int errorCount,
final boolean verbose) {
if (verbose) {
/* show all tasks */
for (TaskRun tRun : finished) {
if (tRun.getState() == Task.State.ERROR) {
describeOneFailedTask(fm, tRun);
continue;
}
fm.format(" Task %3d %" + Task.LONGEST_STATE +
"s at %25s: %s\n",
tRun.getTaskNum(),
tRun.getState(),
FormatUtils.formatDateAndTime(tRun.getEndTime()),
tRun.getTask());
final String details =
tRun.displayTaskDetails(" ");
if (details != null) {
fm.format("%s\n", details);
}
}
return;
}
/* If not verbose, only list the errors */
if (errorCount > 0) {
fm.format("\nFailures:\n");
for (TaskRun tRun : finished) {
if (tRun.getState() == Task.State.ERROR) {
describeOneFailedTask(fm, tRun);
}
}
}
}
void describeOneFailedTask(final Formatter fm,
TaskRun tRun) {
final String failDesc = tRun.getFailureDescription();
if (failDesc == null) {
fm.format(" Task %3d %" + Task.LONGEST_STATE +
"s at %25s: %s\n",
tRun.getTaskNum(),
tRun.getState(),
FormatUtils.formatDateAndTime(tRun.getEndTime()),
tRun.getTask());
} else {
fm.format(" Task %3d %" + Task.LONGEST_STATE +
"s at %25s: %s: %s\n",
tRun.getTaskNum(),
tRun.getState(),
FormatUtils.formatDateAndTime(tRun.getEndTime()),
tRun.getTask(), failDesc);
}
}
/**
* Describe all running tasks, for a status report. Plans can override
* this to provide a more informative, user friendly report for specific
* plans.
*/
@Override
public void describeRunning(Formatter fm,
final List running,
boolean verbose) {
for (TaskRun tRun : running) {
fm.format(" Task %d/%s started at %s\n",
tRun.getTaskNum(), tRun.getTask(),
FormatUtils.formatDateAndTime(tRun.getStartTime()));
}
}
/**
* Describe all pending tasks, for a status report. Plans can override
* this to provide a more informative, user friendly report for specific
* plans.
*/
@Override
public void describeNotStarted(Formatter fm,
final List notStarted,
boolean verbose) {
for (Task t : notStarted) {
fm.format(" Task %s\n", t);
}
}
/**
* Get all the component locks that will serialize access to shards and
* RNs from concurrently executing plans.
* @throws PlanLocksHeldException
*/
@Override
public void getCatalogLocks() throws PlanLocksHeldException {
getPerTaskLocks();
}
/**
* Ask each task to lock what it needs.
* @throws PlanLocksHeldException
*/
protected void getPerTaskLocks() throws PlanLocksHeldException {
for (Task t : taskList.getTasks()) {
t.acquireLocks(planner);
}
}
/**
* By default, no checks to be done. A logger is supplied, instead of
* using the plan's logger, because the plan's logger is not set yet.
*/
@Override
public void preExecuteCheck(boolean force, Logger plannerlogger) {
}
public void upgradeToV3() {
for (PlanRun pr : executionState.getHistory()) {
pr.upgradeToV3(taskList.getTasks());
}
}
/**
* Synchronize when updating task execution information.
*/
synchronized void saveFailure(TaskRun taskRun,
Throwable t,
String problemDescription,
ErrorMessage errorMsg,
String[] cleanupJobs,
Logger logger2) {
taskRun.saveFailure(this, t, problemDescription, errorMsg, cleanupJobs,
logger2);
}
/**
* Synchronize when updating task execution information.
*/
synchronized void setTaskState(TaskRun taskRun,
Task.State taskState,
Logger logger2) {
taskRun.setState(taskState, logger2);
}
@Override
public synchronized void saveFailure(PlanRun planRun,
Throwable t,
String problem,
ErrorMessage errorMsg,
String[] cleanupJobs,
Logger logger2) {
planRun.saveFailure(t, problem, errorMsg, cleanupJobs, logger2);
}
synchronized TaskRun startTask(PlanRun planRun,
Task task,
Logger logger2) {
return planRun.startTask(task, logger2);
}
synchronized void setEndTime(PlanRun planRun) {
planRun.setEndTime();
}
synchronized void incrementEndCount(PlanRun planRun,
Task.State state) {
planRun.incrementEndCount(state);
final PlanProgress planProgress = new PlanProgress(this);
planner.getAdmin().getMonitor().publish(planProgress);
}
synchronized void cleanupEnded(TaskRun taskRun) {
taskRun.cleanupEnded();
}
synchronized void cleanupStarted(TaskRun taskRun) {
taskRun.cleanupStarted();
}
synchronized void saveCleanupFailure(TaskRun taskRun, String info) {
taskRun.saveCleanupFailure(info);
}
@Override
public synchronized ExceptionTransfer getLatestRunExceptionTransfer() {
final PlanRun run = executionState.getLatestPlanRun();
if (run == null) {
return null;
}
return run.getExceptionTransfer();
}
/**
* Default implementation. The default behavior is that a plan is not
* persisted when metadata is updated. Specific plans should override this
* method if they maintain persistent state based on metadata.
*
* @param metadata the metadata being updated
* @return false
*/
@Override
public boolean updatingMetadata(Metadata> metadata) {
return false;
}
public LoginManager getLoginManager() {
return getAdmin().getLoginManager();
}
/**
* Must be supported by any plan that will modify and persist a topology.
* TODO: this is an accommodation to deal with the fact that TopologyPlan,
* introduced in R1 and DeployTopoPlan, introduced in R2, represent
* different class hierarchies. An alternative is to remove this from
* AbstractPlan and introduce a new Plan interface, implemented by both
* TopologyPlan and DeployTopoPlan which would support this method, but
* interfaces have their own issues.
*/
public DeploymentInfo getDeployedInfo() {
throw new UnsupportedOperationException();
}
@Override
public ResourceOwner getOwner() {
return owner;
}
/**
* If a plan has its own return value, it can override this method to
* add fields in the return ObjectNode.
*/
@Override
public ObjectNode getPlanJson() {
return JsonUtils.createObjectNode();
}
/**
* By default operation name is the same as plan name.
*/
@Override
public String getOperation() {
return getName();
}
/**
* If plan state is SUCCEEDED, return CommandSucceeds.
* If plan state is ERROR or INTERRUPTED, return CommandFails.
* Otherwise, return null.
*/
@Override
public CommandResult getCommandResult() {
final State state = executionState.getLatestState();
if (state == State.SUCCEEDED) {
return new CommandResult.CommandSucceeds(getPlanJson().toString());
}
if (state == State.ERROR || state == State.INTERRUPTED) {
final ExceptionTransfer fault =
executionState.getLatestExceptionTransfer();
if (fault == null) {
/* State may be updated ahead of adding ExceptionTransfer.
* No ExceptionTransfer yet. */
return null;
}
return new CommandResult.CommandFails(fault.getDescription(),
fault.getErrorMessage(),
fault.getCleanupJobs());
}
/* Plan hasn't finished yet or was canceled, no result. */
return null;
}
/**
* Returns true if the this plan is a plan generated by the Admin.
*
* @return true if the specified plan is a plan generated by the Admin
*/
@Override
public boolean isSystemPlan() {
return getName().startsWith(SYS_PLAN_PREFIX);
}
@Override
public boolean logicalCompare(Plan other) {
if (getClass() != other.getClass()) {
return false;
}
/*
* For plans to be identical, they need logically equivalent tasks, in
* the same order.
*/
final List tasks = PlanExecutor.getFlatTaskList(this, 0);
final List otherTasks = PlanExecutor.getFlatTaskList(other, 0);
if (tasks.size() != otherTasks.size()) {
return false;
}
for (int i = 0; i < tasks.size(); i++) {
final Task myTask = tasks.get(i);
final Task otherTask = otherTasks.get(i);
if (!myTask.logicalCompare(otherTask)) {
return false;
}
}
return true;
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
this.version = Integer.valueOf(in.readByte());
} catch (EOFException eofe) {
/*
* Reaches the end, regards it as an initial version plan from
* nodes earlier R3.1.0.
*/
initTransientFields();
return;
}
if (version < INITIAL_PLAN_VERSION || version > CURRENT_PLAN_VERSION) {
throw new IOException("Unsupported plan version: " + version);
}
final boolean hasOwner = in.readBoolean();
if (hasOwner) {
this.owner = (ResourceOwner) in.readObject();
}
/* Initialize transient fields from deserialization */
initTransientFields();
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
out.defaultWriteObject();
if (version == null) {
/* Re-writing an old plan object created before R3.1.0 */
version = INITIAL_PLAN_VERSION;
}
out.write(version);
if (owner != null) {
out.writeBoolean(true);
out.writeObject(owner);
} else {
out.writeBoolean(false);
}
}
/* For testing */
void setVersion(int version) {
this.version = version;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy