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

[JENKINS-30101][JENKINS-30175] Simplify persistence design for temporarily offline status #9855

Merged
merged 12 commits into from
Oct 15, 2024
103 changes: 56 additions & 47 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,6 +355,13 @@
*/
@Exported
public OfflineCause getOfflineCause() {
var node = getNode();
if (node != null) {

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 359 is only partially covered, one branch is missing
var temporaryOfflineCause = node.getTemporaryOfflineCause();
if (temporaryOfflineCause != null) {
return temporaryOfflineCause;
}
}
return offlineCause;
}

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

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

/**
Expand Down Expand Up @@ -620,7 +622,7 @@
@Exported
@Override
public boolean isOffline() {
return temporarilyOffline || getChannel() == null;
return isTemporarilyOffline() || getChannel() == null;
}

public final boolean isOnline() {
Expand Down Expand Up @@ -669,41 +671,59 @@
@Exported
@Deprecated
public boolean isTemporarilyOffline() {
return temporarilyOffline;
var node = getNode();
return node != null && node.isTemporarilyOffline();

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 675 is only partially covered, one branch is missing
}

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

/**
* @deprecated as of TODO.
* Use {@link #setTemporaryOfflineCause(OfflineCause)} instead.
*/
@Deprecated
public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The javadoc is lost here especially that the second argument is only considered when the first is true. But maybe this doesn't matter as the method is now set to deprecated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'm dropping existing javadoc to avoid repeats, and only refer to the current methods.

if (cause == null) {
setTemporarilyOffline(temporarilyOffline);
} else {
setTemporaryOfflineCause(cause);
}
}

/**
* 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.
* @param temporaryOfflineCause The reason why the node is being put offline.
* If null, this cancels the status
*/
public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) {
offlineCause = temporarilyOffline ? cause : null;
this.temporarilyOffline = temporarilyOffline;
Node node = getNode();
if (node != null) {
node.setTemporaryOfflineCause(offlineCause);
public void setTemporaryOfflineCause(@CheckForNull OfflineCause temporaryOfflineCause) {
var node = getNode();
if (node == null) {

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 709 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 710 in core/src/main/java/hudson/model/Computer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 710 is not covered by tests
}
synchronized (statusChangeLock) {
statusChangeLock.notifyAll();
node.setTemporaryOfflineCause(temporaryOfflineCause);
}

@SuppressWarnings("unused") // used by setOfflineCause.jelly
public String getTemporaryOfflineCauseReason() {
var node = getNode();
if (node == null) {

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 718 is only partially covered, one branch is missing
// Node was deleted; computer still exists
return null;

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 720 is not covered by tests
}
if (temporarilyOffline) {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOffline(this, cause));
} else {
Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOnline(this));
var cause = node.getTemporaryOfflineCause();
if (cause instanceof OfflineCause.UserCause userCause) {

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 723 is only partially covered, one branch is missing
return userCause.getMessage();
}
return cause != null ? cause.toString() : "";

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 726 is not covered by tests
}

@Exported
Expand Down Expand Up @@ -785,16 +805,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 +1406,23 @@

@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 {
var node = getNode();
if (node == null) {

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1410 is only partially covered, one branch is missing
return HttpResponses.notFound();

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1411 is not covered by tests
}
if (node.isTemporarilyOffline()) {
checkPermission(CONNECT);
setTemporarilyOffline(!temporarilyOffline, null);
setTemporaryOfflineCause(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));
setTemporaryOfflineCause(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.setTemporaryOfflineCause(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.setTemporaryOfflineCause(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
29 changes: 24 additions & 5 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 Expand Up @@ -136,15 +151,15 @@ public static class UserCause extends SimpleOfflineCause {
// null when unknown
private /*final*/ @CheckForNull String userId;

private final String message;

public UserCause(@CheckForNull User user, @CheckForNull String message) {
this(
user != null ? user.getId() : null,
message != null ? " : " + message : ""
);
this(user != null ? user.getId() : null, message);
}

private UserCause(String userId, String message) {
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS2.getName(), message));
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS2.getName(), message != null ? " : " + message : ""));
this.message = message;
this.userId = userId;
}

Expand All @@ -155,6 +170,10 @@ public User getUser() {
;
}

public String getMessage() {
return message;
}

// Storing the User in a filed was a mistake, switch to userId
private Object readResolve() throws ObjectStreamException {
if (user != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ THE SOFTWARE.
${%blurb}
</p>
<form method="post" action="changeOfflineCause">
<f:textarea name="offlineMessage" value="${it.getOfflineCauseReason()}"/>
<f:textarea name="offlineMessage" value="${it.temporaryOfflineCauseReason}"/>
<p>
<f:submit value="${%submit}" />
</p>
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
5 changes: 4 additions & 1 deletion test/src/test/java/hudson/model/NodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
Expand Down Expand Up @@ -102,7 +103,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 All @@ -115,13 +116,15 @@ public void testOfflineCause() throws Exception {
try (ACLContext ignored = ACL.as2(someone.impersonate2())) {
computer.doToggleOffline("original message");
cause = (OfflineCause.UserCause) computer.getOfflineCause();
assertThat(computer.getTemporaryOfflineCauseReason(), is("original message"));
assertTrue(cause.toString(), cause.toString().matches("^.*?Disconnected by [email protected] : original message"));
assertEquals(someone, cause.getUser());
}
final User root = User.getOrCreateByIdOrFullName("root@localhost");
try (ACLContext ignored = ACL.as2(root.impersonate2())) {
computer.doChangeOfflineCause("new message");
cause = (OfflineCause.UserCause) computer.getOfflineCause();
assertThat(computer.getTemporaryOfflineCauseReason(), is("new message"));
assertTrue(cause.toString(), cause.toString().matches("^.*?Disconnected by root@localhost : new message"));
assertEquals(root, cause.getUser());

Expand Down
Loading