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
104 changes: 57 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 @@ public AnnotatedLargeText<Computer> getLogText() {
*/
@Exported
public OfflineCause getOfflineCause() {
var node = getNode();
if (node != null) {
var temporaryOfflineCause = node.getTemporaryOfflineCause();
if (temporaryOfflineCause != null) {
return temporaryOfflineCause;
}
}
return offlineCause;
}

Expand All @@ -371,6 +373,7 @@ public boolean hasOfflineCause() {
@Exported
@Override
public String getOfflineCauseReason() {
var offlineCause = getOfflineCause();
if (offlineCause == null) {
return "";
}
Expand Down Expand Up @@ -549,7 +552,7 @@ public void cliDisconnect(String cause) throws ExecutionException, InterruptedEx
@Deprecated
public void cliOffline(String cause) throws ExecutionException, InterruptedException {
checkPermission(DISCONNECT);
setTemporarilyOffline(true, new ByCLI(cause));
setTemporaryOfflineCause(new ByCLI(cause));
}

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

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

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

/**
* @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) {
throw new IllegalStateException("Can't set a temporary offline cause if the node has been removed");
}
synchronized (statusChangeLock) {
statusChangeLock.notifyAll();
node.setTemporaryOfflineCause(temporaryOfflineCause);
}

@SuppressWarnings("unused") // used by setOfflineCause.jelly
public String getTemporaryOfflineCauseReason() {
var node = getNode();
if (node == null) {
// Node was deleted; computer still exists
return null;
}
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) {
return userCause.getMessage();
}
return cause != null ? cause.toString() : "";
}

@Exported
Expand Down Expand Up @@ -785,16 +806,6 @@ protected void setNode(Node node) {
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 +1407,23 @@ public void doRssLatest(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExce

@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) {
return HttpResponses.notFound();
}
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,7 +233,7 @@ public boolean isIgnored() {
*/
protected boolean markOnline(Computer c) {
if (isIgnored() || c.isOnline()) return false; // noop
c.setTemporarilyOffline(false, null);
c.setTemporaryOfflineCause(null);
return true;
}

Expand All @@ -247,7 +247,7 @@ protected boolean markOnline(Computer c) {
protected boolean markOffline(Computer c, OfflineCause oc) {
if (isIgnored() || c.isTemporarilyOffline()) return false; // noop

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

// 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
6 changes: 5 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,16 @@ public void testOfflineCause() throws Exception {
try (ACLContext ignored = ACL.as2(someone.impersonate2())) {
computer.doToggleOffline("original message");
cause = (OfflineCause.UserCause) computer.getOfflineCause();
assertThat(computer.getOfflineCauseReason(), is("original message"));
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