Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify persistence design for temporarily offline status #9855

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 44 additions & 51 deletions core/src/main/java/hudson/model/Computer.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,6 @@

private long connectTime = 0;

/**
* True if Jenkins shouldn't start new builds on this node.
*/
private boolean temporarilyOffline;

/**
* {@link Node} object may be created and deleted independently
* from this object.
Expand Down Expand Up @@ -360,7 +355,8 @@
*/
@Exported
public OfflineCause getOfflineCause() {
return offlineCause;
var temporaryOfflineCause = getNodeOrDie().getTemporaryOfflineCause();
return temporaryOfflineCause == null ? offlineCause : temporaryOfflineCause;
Comment on lines +358 to +359
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retaining previous behaviour : if a temporary offline cause is defined, from outside PoV, it replaces the technical offline cause.

}

@Override
Expand Down Expand Up @@ -549,7 +545,7 @@
@Deprecated
public void cliOffline(String cause) throws ExecutionException, InterruptedException {
checkPermission(DISCONNECT);
setTemporarilyOffline(true, new ByCLI(cause));
setTemporarilyOfflineCause(new ByCLI(cause));
}

/**
Expand All @@ -558,7 +554,7 @@
@Deprecated
public void cliOnline() throws ExecutionException, InterruptedException {
checkPermission(CONNECT);
setTemporarilyOffline(false, null);
setTemporarilyOfflineCause(null);
}

/**
Expand Down Expand Up @@ -606,6 +602,15 @@
return j.getNode(nodeName);
}

@NonNull
private Node getNodeOrDie() {
var node = nodeName == null ? Jenkins.get() : Jenkins.get().getNode(nodeName);
if (node == null) {

Check warning on line 608 in core/src/main/java/hudson/model/Computer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 608 is only partially covered, one branch is missing
throw new IllegalStateException("Can't set a temporary offline cause if the node has been removed");

Check warning on line 609 in core/src/main/java/hudson/model/Computer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 609 is not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this nullness check needs to be moved up into the callers, since some are just reading the status, not trying to set it, right?

}
return node;
}

@Exported
public LoadStatistics getLoadStatistics() {
return LabelAtom.get(nodeName != null ? nodeName : Jenkins.get().getSelfLabel().toString()).loadStatistics;
Expand All @@ -620,7 +625,7 @@
@Exported
@Override
public boolean isOffline() {
return temporarilyOffline || getChannel() == null;
return getNodeOrDie().isTemporarilyOffline() || getChannel() == null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example this will throw an ISE from the whole api/json endpoint of a computer corresponding to a recently removed permanent agent, which is a legal situation.

Suggested change
return getNodeOrDie().isTemporarilyOffline() || getChannel() == null;
var node = getNode();
return node != null && node.isTemporarilyOffline() || getChannel() == null;

getNodeOrDie could probably just be removed, and callers of getNode should decide how best to handle nullness; even for an offline setter, an ISE seems inappropriate—either just ignore the request (harmless, soon to be meaningless) or return a 404 or whatever.

}

public final boolean isOnline() {
Expand Down Expand Up @@ -669,43 +674,46 @@
@Exported
@Deprecated
public boolean isTemporarilyOffline() {
return temporarilyOffline;
return getNodeOrDie().isTemporarilyOffline();
}

/**
* @deprecated as of 1.320.
* Use {@link #setTemporarilyOffline(boolean, OfflineCause)}
* Use {@link #setTemporarilyOfflineCause(OfflineCause)}
*/
@Deprecated
public void setTemporarilyOffline(boolean temporarilyOffline) {
setTemporarilyOffline(temporarilyOffline, null);
setTemporarilyOfflineCause(temporarilyOffline ? new OfflineCause.LegacyOfflineCause() : null);
}

/**
* Marks the computer as temporarily offline. This retains the underlying
* {@link Channel} connection, but prevent builds from executing.
*
* @param cause
* If the first argument is true, specify the reason why the node is being put
* offline.
* @deprecated as of TODO.
* Use {@link #setTemporarilyOfflineCause(OfflineCause)} instead.
*/
@Deprecated
public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) {
offlineCause = temporarilyOffline ? cause : null;
this.temporarilyOffline = temporarilyOffline;
Node node = getNode();
if (node != null) {
node.setTemporaryOfflineCause(offlineCause);
}
synchronized (statusChangeLock) {
statusChangeLock.notifyAll();
}
if (temporarilyOffline) {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOffline(this, cause));
if (cause == null) {
setTemporarilyOffline(temporarilyOffline);
} else {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOnline(this));
setTemporarilyOfflineCause(cause);
}
}

/**
* Marks the computer as temporarily offline. This retains the underlying
* {@link Channel} connection, but prevent builds from executing.
*
* @param temporarilyOfflineCause The reason why the node is being put offline.
* If null, this cancels the status
*/
public void setTemporarilyOfflineCause(@CheckForNull OfflineCause temporarilyOfflineCause) {
getNodeOrDie().setTemporaryOfflineCause(temporarilyOfflineCause);
}

public OfflineCause getTemporarilyOfflineCause() {
return getNodeOrDie().getTemporaryOfflineCause();

Check warning on line 714 in core/src/main/java/hudson/model/Computer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 714 is not covered by tests
}

@Exported
@Override
public String getIcon() {
Expand Down Expand Up @@ -785,16 +793,6 @@
this.nodeName = null;

setNumExecutors(node.getNumExecutors());
if (this.temporarilyOffline) {
// When we get a new node, push our current temp offline
// status to it (as the status is not carried across
// configuration changes that recreate the node).
// Since this is also called the very first time this
// Computer is created, avoid pushing an empty status
// as that could overwrite any status that the Node
// brought along from its persisted config data.
node.setTemporaryOfflineCause(this.offlineCause);
}
}

/**
Expand Down Expand Up @@ -1396,24 +1394,19 @@

@RequirePOST
public HttpResponse doToggleOffline(@QueryParameter String offlineMessage) throws IOException, ServletException {
if (!temporarilyOffline) {
checkPermission(DISCONNECT);
offlineMessage = Util.fixEmptyAndTrim(offlineMessage);
setTemporarilyOffline(!temporarilyOffline,
new OfflineCause.UserCause(User.current(), offlineMessage));
} else {
if (getNodeOrDie().isTemporarilyOffline()) {
checkPermission(CONNECT);
setTemporarilyOffline(!temporarilyOffline, null);
setTemporarilyOfflineCause(null);
return HttpResponses.redirectToDot();
} else {
return doChangeOfflineCause(offlineMessage);
}
return HttpResponses.redirectToDot();
}

@RequirePOST
public HttpResponse doChangeOfflineCause(@QueryParameter String offlineMessage) throws IOException, ServletException {
checkPermission(DISCONNECT);
offlineMessage = Util.fixEmptyAndTrim(offlineMessage);
setTemporarilyOffline(true,
new OfflineCause.UserCause(User.current(), offlineMessage));
setTemporarilyOfflineCause(new OfflineCause.UserCause(User.current(), Util.fixEmptyAndTrim(offlineMessage)));
return HttpResponses.redirectToDot();
}

Expand Down
27 changes: 9 additions & 18 deletions core/src/main/java/hudson/model/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.FileSystemProvisioner;
Expand Down Expand Up @@ -69,6 +68,7 @@
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.model.Nodes;
import jenkins.util.Listeners;
import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
Expand Down Expand Up @@ -264,25 +264,11 @@ public void onLoad(Nodes parent, String name) {
setNodeName(name);
}

/**
* Let Nodes be aware of the lifecycle of their own {@link Computer}.
*/
@Extension
public static class InternalComputerListener extends ComputerListener {
@Override
public void onOnline(Computer c, TaskListener listener) {
Node node = c.getNode();

// At startup, we need to restore any previously in-effect temp offline cause.
// We wait until the computer is started rather than getting the data to it sooner
// so that the normal computer start up processing works as expected.
if (node != null && node.temporaryOfflineCause != null && node.temporaryOfflineCause != c.getOfflineCause()) {
c.setTemporarilyOffline(true, node.temporaryOfflineCause);
}
}
boolean isTemporarilyOffline() {
return temporaryOfflineCause != null;
}

private OfflineCause temporaryOfflineCause;
private volatile OfflineCause temporaryOfflineCause;

/**
* Enable a {@link Computer} to inform its node when it is taken
Expand All @@ -294,6 +280,11 @@ void setTemporaryOfflineCause(OfflineCause cause) {
temporaryOfflineCause = cause;
save();
}
if (temporaryOfflineCause != null) {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOffline(toComputer(), temporaryOfflineCause));
} else {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOnline(toComputer()));
}
} catch (java.io.IOException e) {
LOGGER.warning("Unable to complete save, temporary offline status will not be persisted: " + e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,21 +233,21 @@
*/
protected boolean markOnline(Computer c) {
if (isIgnored() || c.isOnline()) return false; // noop
c.setTemporarilyOffline(false, null);
c.setTemporarilyOfflineCause(null);
return true;
}

/**
* Utility method to mark the computer offline for derived classes.
*
* @return true
* if the node was actually taken offline by this act (as opposed to us deciding not to do it,
* or the computer already marked offline.)
*/
protected boolean markOffline(Computer c, OfflineCause oc) {
if (isIgnored() || c.isTemporarilyOffline()) return false; // noop

c.setTemporarilyOffline(true, oc);
c.setTemporarilyOfflineCause(oc);

Check warning on line 250 in core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 236-250 are not covered by tests

// notify the admin
MonitorMarkedNodeOffline no = AdministrativeMonitor.all().get(MonitorMarkedNodeOffline.class);
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/hudson/slaves/OfflineCause.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import java.util.Date;
import jenkins.model.Jenkins;
import org.jvnet.localizer.Localizable;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

Expand Down Expand Up @@ -71,6 +73,19 @@ public long getTimestamp() {
return new Date(timestamp);
}

/**
* @deprecated Only exists for backward compatibility.
* @see Computer#setTemporarilyOffline(boolean)
*/
@Deprecated
@Restricted(NoExternalUse.class)
public static class LegacyOfflineCause extends OfflineCause {
@Exported(name = "description") @Override
public String toString() {
return "";
}
}

/**
* {@link OfflineCause} that renders a static text,
* but without any further UI.
Expand Down
4 changes: 2 additions & 2 deletions test/src/test/java/hudson/cli/OfflineNodeCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public void offlineNodeShouldSucceedOnOfflineNode() throws Exception {
slave.toComputer().setTemporarilyOffline(true, null);
assertThat(slave.toComputer().isOffline(), equalTo(true));
assertThat(slave.toComputer().isTemporarilyOffline(), equalTo(true));
assertThat(slave.toComputer().getOfflineCause(), equalTo(null));
assertThat(slave.toComputer().getOfflineCause(), instanceOf(OfflineCause.LegacyOfflineCause.class));

final CLICommandInvoker.Result result = command
.authorizedTo(Computer.DISCONNECT, Jenkins.READ)
Expand Down Expand Up @@ -177,7 +177,7 @@ public void offlineNodeShouldSucceedOnOfflineNodeWithCause() throws Exception {
slave.toComputer().setTemporarilyOffline(true, null);
assertThat(slave.toComputer().isOffline(), equalTo(true));
assertThat(slave.toComputer().isTemporarilyOffline(), equalTo(true));
assertThat(slave.toComputer().getOfflineCause(), equalTo(null));
assertThat(slave.toComputer().getOfflineCause(), instanceOf(OfflineCause.LegacyOfflineCause.class));

final CLICommandInvoker.Result result = command
.authorizedTo(Computer.DISCONNECT, Jenkins.READ)
Expand Down
2 changes: 1 addition & 1 deletion test/src/test/java/hudson/model/NodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public void testSetTemporaryOfflineCause() throws Exception {
assertEquals("Node should have offline cause which was set.", cause, node.toComputer().getOfflineCause());
OfflineCause cause2 = new OfflineCause.ByCLI("another message");
node.setTemporaryOfflineCause(cause2);
assertEquals("Node should have original offline cause after setting another.", cause, node.toComputer().getOfflineCause());
assertEquals("Node should have the new offline cause.", cause2, node.toComputer().getOfflineCause());
}

@Test
Expand Down
Loading