This is an automated email from the ASF dual-hosted git repository.

pauloricardomg pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 969cbcd3 CASSSIDECAR-376: Update OperationalJob to support 
cluster-wide operations (#327)
969cbcd3 is described below

commit 969cbcd37bd948a87d2ade9674f94ec4a4ee6b94
Author: Brandon Ho <[email protected]>
AuthorDate: Fri May 15 10:19:24 2026 -0400

    CASSSIDECAR-376: Update OperationalJob to support cluster-wide operations 
(#327)
    
    CASSSIDECAR-376: Update OperationalJob class to support cluster-wide 
operations
    
    - Add nodeId, startTime, lastUpdate, and node tracking lists to 
OperationalJob
    - Add hostId to NodeSettings; remove separate 
CassandraAdapterDelegate.hostId()
    - Add Builder pattern to OperationalJobResponse and extend with new fields
    - Update handlers to use new fields and delegate.nodeSettings().hostId()
    - Use Instant for startTime/lastUpdate with ISO-8601 JSON serialization
    
    patch by Brandon Ho; reviewed by Paulo Motta, Francisco Guerrero for 
CASSSIDECAR-376
---
 .../sidecar/common/response/NodeSettings.java      |  25 +-
 .../common/response/OperationalJobResponse.java    | 254 ++++++++++++++++++++-
 .../common/utils/InstantIso8601Deserializer.java   |  40 ++++
 .../common/utils/InstantIso8601Serializer.java     |  46 ++++
 .../response/OperationalJobResponseTest.java       | 145 ++++++++++++
 .../sidecar/cluster/CassandraAdapterDelegate.java  |   8 +
 .../handlers/ListOperationalJobsHandler.java       |  14 +-
 .../sidecar/handlers/NodeDecommissionHandler.java  |  10 +-
 .../sidecar/handlers/NodeDrainHandler.java         |  10 +-
 .../sidecar/handlers/NodeMoveHandler.java          |  10 +-
 .../cassandra/sidecar/job/NodeDecommissionJob.java |   8 +-
 .../apache/cassandra/sidecar/job/NodeDrainJob.java |   8 +-
 .../apache/cassandra/sidecar/job/NodeMoveJob.java  |   8 +-
 .../cassandra/sidecar/job/OperationalJob.java      | 131 +++++++++++
 .../sidecar/utils/OperationalJobUtils.java         |  26 ++-
 .../org/apache/cassandra/sidecar/TestModule.java   |   2 +
 .../CassandraClientTokenRingProviderTest.java      |   2 +
 .../cassandra/sidecar/handlers/CommonTest.java     |   5 +
 .../sidecar/handlers/NodeDrainHandlerTest.java     |   6 +
 .../sidecar/handlers/NodeMoveHandlerTest.java      |   6 +
 .../cassandra/sidecar/job/OperationalJobTest.java  |   2 +
 21 files changed, 751 insertions(+), 15 deletions(-)

diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/NodeSettings.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/NodeSettings.java
index ab988be1..2fc83a9a 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/NodeSettings.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/NodeSettings.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.cassandra.sidecar.common.DataObjectBuilder;
@@ -48,6 +49,8 @@ public class NodeSettings
     private final Set<String> tokens;
     @JsonProperty("sidecar")
     private final Map<String, String> sidecar;
+    @JsonProperty("hostId")
+    private final UUID hostId;
 
     /**
      * Constructs a new {@link NodeSettings}.
@@ -71,6 +74,7 @@ public class NodeSettings
         rpcPort = builder.rpcPort;
         tokens = builder.tokens;
         sidecar = builder.sidecar;
+        hostId = builder.hostId;
     }
 
     @JsonProperty("releaseVersion")
@@ -120,6 +124,12 @@ public class NodeSettings
         return tokens;
     }
 
+    @JsonProperty("hostId")
+    public UUID hostId()
+    {
+        return hostId;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -142,6 +152,7 @@ public class NodeSettings
                && Objects.equals(this.rpcAddress, that.rpcAddress)
                && Objects.equals(this.rpcPort, that.rpcPort)
                && Objects.equals(this.tokens, that.tokens)
+               && Objects.equals(this.hostId, that.hostId)
         ;
     }
 
@@ -151,7 +162,7 @@ public class NodeSettings
     @Override
     public int hashCode()
     {
-        return Objects.hash(releaseVersion, partitioner, sidecar, datacenter, 
rpcAddress, rpcPort, tokens);
+        return Objects.hash(releaseVersion, partitioner, sidecar, datacenter, 
rpcAddress, rpcPort, tokens, hostId);
     }
 
     /**
@@ -174,6 +185,7 @@ public class NodeSettings
         private int rpcPort;
         private Set<String> tokens;
         private Map<String, String> sidecar;
+        private UUID hostId;
 
         private Builder()
         {
@@ -262,6 +274,17 @@ public class NodeSettings
             return update(b -> b.sidecar = sidecar);
         }
 
+        /**
+         * Sets the {@code hostId} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param hostId the {@code hostId} to set
+         * @return a reference to this Builder
+         */
+        public Builder hostId(UUID hostId)
+        {
+            return update(b -> b.hostId = hostId);
+        }
+
         /**
          * Sets the {@code sidecarVersion} in the {@code sidecar} map and 
returns a reference to this Builder
          * enabling method chaining.
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponse.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponse.java
index 271f398a..d33c9886 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponse.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponse.java
@@ -18,13 +18,20 @@
 
 package org.apache.cassandra.sidecar.common.response;
 
+import java.time.Instant;
+import java.util.List;
 import java.util.UUID;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.common.data.OperationalJobStatus;
+import org.apache.cassandra.sidecar.common.utils.InstantIso8601Deserializer;
+import org.apache.cassandra.sidecar.common.utils.InstantIso8601Serializer;
 
 /**
  * Response structure of the operational jobs API
@@ -37,17 +44,61 @@ public class OperationalJobResponse
     private final OperationalJobStatus status;
     private final String operation;
     private final String reason;
+    @JsonSerialize(using = InstantIso8601Serializer.class)
+    @JsonDeserialize(using = InstantIso8601Deserializer.class)
+    private final Instant startTime;
+    private final List<UUID> nodesPending;
+    private final List<UUID> nodesExecuting;
+    private final List<UUID> nodesSucceeded;
+    private final List<UUID> nodesFailed;
+    @JsonSerialize(using = InstantIso8601Serializer.class)
+    @JsonDeserialize(using = InstantIso8601Deserializer.class)
+    private final Instant lastUpdate;
 
     @JsonCreator
     public OperationalJobResponse(@JsonProperty("jobId") UUID jobId,
                                   @JsonProperty("jobStatus") 
OperationalJobStatus status,
                                   @JsonProperty("operation") String operation,
-                                  @JsonProperty("reason") String reason)
+                                  @JsonProperty("reason") String reason,
+                                  @JsonProperty("startTime") Instant startTime,
+                                  @JsonProperty("nodesPending") List<UUID> 
nodesPending,
+                                  @JsonProperty("nodesExecuting") List<UUID> 
nodesExecuting,
+                                  @JsonProperty("nodesSucceeded") List<UUID> 
nodesSucceeded,
+                                  @JsonProperty("nodesFailed") List<UUID> 
nodesFailed,
+                                  @JsonProperty("lastUpdate") Instant 
lastUpdate)
     {
         this.jobId = jobId;
         this.status = status;
         this.operation = operation;
         this.reason = reason;
+        this.startTime = startTime;
+        this.nodesPending = nodesPending;
+        this.nodesExecuting = nodesExecuting;
+        this.nodesSucceeded = nodesSucceeded;
+        this.nodesFailed = nodesFailed;
+        this.lastUpdate = lastUpdate;
+    }
+
+    private OperationalJobResponse(Builder builder)
+    {
+        this.jobId = builder.jobId;
+        this.status = builder.status;
+        this.operation = builder.operation;
+        this.reason = builder.reason;
+        this.startTime = builder.startTime;
+        this.nodesPending = builder.nodesPending;
+        this.nodesExecuting = builder.nodesExecuting;
+        this.nodesSucceeded = builder.nodesSucceeded;
+        this.nodesFailed = builder.nodesFailed;
+        this.lastUpdate = builder.lastUpdate;
+    }
+
+    /**
+     * @return a new {@link Builder} instance
+     */
+    public static Builder builder()
+    {
+        return new Builder();
     }
 
     /**
@@ -86,4 +137,205 @@ public class OperationalJobResponse
         return reason;
     }
 
+    /**
+     * @return the time this job started execution
+     */
+    @JsonProperty("startTime")
+    public Instant startTime()
+    {
+        return startTime;
+    }
+
+    /**
+     * @return the list of nodes pending execution
+     */
+    @JsonProperty("nodesPending")
+    public List<UUID> nodesPending()
+    {
+        return nodesPending;
+    }
+
+    /**
+     * @return the list of nodes currently executing
+     */
+    @JsonProperty("nodesExecuting")
+    public List<UUID> nodesExecuting()
+    {
+        return nodesExecuting;
+    }
+
+    /**
+     * @return the list of nodes that have succeeded
+     */
+    @JsonProperty("nodesSucceeded")
+    public List<UUID> nodesSucceeded()
+    {
+        return nodesSucceeded;
+    }
+
+    /**
+     * @return the list of nodes that have failed
+     */
+    @JsonProperty("nodesFailed")
+    public List<UUID> nodesFailed()
+    {
+        return nodesFailed;
+    }
+
+    /**
+     * @return the time of the last status update
+     */
+    @JsonProperty("lastUpdate")
+    public Instant lastUpdate()
+    {
+        return lastUpdate;
+    }
+
+    /**
+     * {@code OperationalJobResponse} builder static inner class.
+     */
+    public static final class Builder implements DataObjectBuilder<Builder, 
OperationalJobResponse>
+    {
+        private UUID jobId;
+        private OperationalJobStatus status;
+        private String operation;
+        private String reason;
+        private Instant startTime;
+        private List<UUID> nodesPending;
+        private List<UUID> nodesExecuting;
+        private List<UUID> nodesSucceeded;
+        private List<UUID> nodesFailed;
+        private Instant lastUpdate;
+
+        private Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code jobId} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param jobId the {@code jobId} to set
+         * @return a reference to this Builder
+         */
+        public Builder jobId(UUID jobId)
+        {
+            return update(b -> b.jobId = jobId);
+        }
+
+        /**
+         * Sets the {@code status} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param status the {@code status} to set
+         * @return a reference to this Builder
+         */
+        public Builder status(OperationalJobStatus status)
+        {
+            return update(b -> b.status = status);
+        }
+
+        /**
+         * Sets the {@code operation} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param operation the {@code operation} to set
+         * @return a reference to this Builder
+         */
+        public Builder operation(String operation)
+        {
+            return update(b -> b.operation = operation);
+        }
+
+        /**
+         * Sets the {@code reason} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param reason the {@code reason} to set
+         * @return a reference to this Builder
+         */
+        public Builder reason(String reason)
+        {
+            return update(b -> b.reason = reason);
+        }
+
+        /**
+         * Sets the {@code startTime} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param startTime the {@code startTime} to set
+         * @return a reference to this Builder
+         */
+        public Builder startTime(Instant startTime)
+        {
+            return update(b -> b.startTime = startTime);
+        }
+
+        /**
+         * Sets the {@code nodesPending} and returns a reference to this 
Builder enabling method chaining.
+         *
+         * @param nodesPending the {@code nodesPending} to set
+         * @return a reference to this Builder
+         */
+        public Builder nodesPending(List<UUID> nodesPending)
+        {
+            return update(b -> b.nodesPending = nodesPending);
+        }
+
+        /**
+         * Sets the {@code nodesExecuting} and returns a reference to this 
Builder enabling method chaining.
+         *
+         * @param nodesExecuting the {@code nodesExecuting} to set
+         * @return a reference to this Builder
+         */
+        public Builder nodesExecuting(List<UUID> nodesExecuting)
+        {
+            return update(b -> b.nodesExecuting = nodesExecuting);
+        }
+
+        /**
+         * Sets the {@code nodesSucceeded} and returns a reference to this 
Builder enabling method chaining.
+         *
+         * @param nodesSucceeded the {@code nodesSucceeded} to set
+         * @return a reference to this Builder
+         */
+        public Builder nodesSucceeded(List<UUID> nodesSucceeded)
+        {
+            return update(b -> b.nodesSucceeded = nodesSucceeded);
+        }
+
+        /**
+         * Sets the {@code nodesFailed} and returns a reference to this 
Builder enabling method chaining.
+         *
+         * @param nodesFailed the {@code nodesFailed} to set
+         * @return a reference to this Builder
+         */
+        public Builder nodesFailed(List<UUID> nodesFailed)
+        {
+            return update(b -> b.nodesFailed = nodesFailed);
+        }
+
+        /**
+         * Sets the {@code lastUpdate} and returns a reference to this Builder 
enabling method chaining.
+         *
+         * @param lastUpdate the {@code lastUpdate} to set
+         * @return a reference to this Builder
+         */
+        public Builder lastUpdate(Instant lastUpdate)
+        {
+            return update(b -> b.lastUpdate = lastUpdate);
+        }
+
+        /**
+         * Returns a {@code OperationalJobResponse} built from the parameters 
previously set.
+         *
+         * @return a {@code OperationalJobResponse} built with parameters of 
this {@code Builder}
+         */
+        @Override
+        public OperationalJobResponse build()
+        {
+            return new OperationalJobResponse(this);
+        }
+    }
 }
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Deserializer.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Deserializer.java
new file mode 100644
index 00000000..2d0d94c5
--- /dev/null
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Deserializer.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.utils;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+/**
+ * Deserializes an ISO-8601 string (e.g. {@code 2026-05-12T14:30:00Z}) into a 
{@link java.time.Instant}.
+ * This avoids a dependency on {@code 
com.fasterxml.jackson.datatype:jackson-datatype-jsr310}.
+ */
+public class InstantIso8601Deserializer extends JsonDeserializer<Instant>
+{
+    @Override
+    public Instant deserialize(JsonParser p, DeserializationContext ctx) 
throws IOException
+    {
+        String text = p.getText();
+        return text == null ? null : Instant.parse(text);
+    }
+}
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Serializer.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Serializer.java
new file mode 100644
index 00000000..d5e4d7f9
--- /dev/null
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/utils/InstantIso8601Serializer.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.utils;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * Serializes a {@link java.time.Instant} as an ISO-8601 string (e.g. {@code 
2026-05-12T14:30:00Z}).
+ * This avoids a dependency on {@code 
com.fasterxml.jackson.datatype:jackson-datatype-jsr310}.
+ */
+public class InstantIso8601Serializer extends JsonSerializer<Instant>
+{
+    @Override
+    public void serialize(Instant value, JsonGenerator gen, SerializerProvider 
provider) throws IOException
+    {
+        if (value == null)
+        {
+            gen.writeNull();
+        }
+        else
+        {
+            gen.writeString(value.toString());
+        }
+    }
+}
diff --git 
a/client-common/src/test/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponseTest.java
 
b/client-common/src/test/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponseTest.java
new file mode 100644
index 00000000..811abe98
--- /dev/null
+++ 
b/client-common/src/test/java/org/apache/cassandra/sidecar/common/response/OperationalJobResponseTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.response;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.cassandra.sidecar.common.data.OperationalJobStatus;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link OperationalJobResponse} JSON serialization and 
deserialization.
+ */
+class OperationalJobResponseTest
+{
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Test
+    void testSerializeStartTimeAsIso8601() throws Exception
+    {
+        Instant startTime = Instant.parse("2026-05-12T14:30:00Z");
+        OperationalJobResponse response = OperationalJobResponse.builder()
+                                                                
.jobId(UUID.randomUUID())
+                                                                
.status(OperationalJobStatus.RUNNING)
+                                                                
.operation("drain")
+                                                                
.startTime(startTime)
+                                                                .build();
+
+        String json = objectMapper.writeValueAsString(response);
+
+        assertThat(json).contains("\"startTime\":\"2026-05-12T14:30:00Z\"");
+    }
+
+    @Test
+    void testSerializeLastUpdateAsIso8601() throws Exception
+    {
+        Instant lastUpdate = Instant.parse("2026-05-12T14:35:00Z");
+        OperationalJobResponse response = OperationalJobResponse.builder()
+                                                                
.jobId(UUID.randomUUID())
+                                                                
.status(OperationalJobStatus.RUNNING)
+                                                                
.operation("drain")
+                                                                
.lastUpdate(lastUpdate)
+                                                                .build();
+
+        String json = objectMapper.writeValueAsString(response);
+
+        assertThat(json).contains("\"lastUpdate\":\"2026-05-12T14:35:00Z\"");
+    }
+
+    @Test
+    void testDeserializeIso8601ToInstant() throws Exception
+    {
+        String json = "{\"jobId\":\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\","
+                      + "\"jobStatus\":\"RUNNING\","
+                      + "\"operation\":\"drain\","
+                      + "\"startTime\":\"2026-05-12T14:30:00Z\","
+                      + "\"lastUpdate\":\"2026-05-12T14:35:00Z\"}";
+
+        OperationalJobResponse response = objectMapper.readValue(json, 
OperationalJobResponse.class);
+
+        
assertThat(response.startTime()).isEqualTo(Instant.parse("2026-05-12T14:30:00Z"));
+        
assertThat(response.lastUpdate()).isEqualTo(Instant.parse("2026-05-12T14:35:00Z"));
+    }
+
+    @Test
+    void testRoundTripSerialization() throws Exception
+    {
+        Instant startTime = Instant.parse("2026-05-12T14:30:00Z");
+        Instant lastUpdate = Instant.parse("2026-05-12T14:35:00Z");
+        UUID jobId = UUID.randomUUID();
+        UUID nodeId = UUID.randomUUID();
+        List<UUID> nodesSucceeded = Arrays.asList(nodeId);
+
+        OperationalJobResponse original = OperationalJobResponse.builder()
+                                                                 .jobId(jobId)
+                                                                 
.status(OperationalJobStatus.SUCCEEDED)
+                                                                 
.operation("decommission")
+                                                                 
.startTime(startTime)
+                                                                 
.lastUpdate(lastUpdate)
+                                                                 
.nodesSucceeded(nodesSucceeded)
+                                                                 .build();
+
+        String json = objectMapper.writeValueAsString(original);
+        OperationalJobResponse deserialized = objectMapper.readValue(json, 
OperationalJobResponse.class);
+
+        assertThat(deserialized.jobId()).isEqualTo(jobId);
+        
assertThat(deserialized.status()).isEqualTo(OperationalJobStatus.SUCCEEDED);
+        assertThat(deserialized.operation()).isEqualTo("decommission");
+        assertThat(deserialized.startTime()).isEqualTo(startTime);
+        assertThat(deserialized.lastUpdate()).isEqualTo(lastUpdate);
+        assertThat(deserialized.nodesSucceeded()).isEqualTo(nodesSucceeded);
+    }
+
+    @Test
+    void testNullTimesOmittedFromJson() throws Exception
+    {
+        OperationalJobResponse response = OperationalJobResponse.builder()
+                                                                
.jobId(UUID.randomUUID())
+                                                                
.status(OperationalJobStatus.CREATED)
+                                                                
.operation("move")
+                                                                .build();
+
+        String json = objectMapper.writeValueAsString(response);
+
+        assertThat(json).doesNotContain("startTime");
+        assertThat(json).doesNotContain("lastUpdate");
+    }
+
+    @Test
+    void testDeserializeUnknownFieldsIgnored() throws Exception
+    {
+        String json = "{\"jobId\":\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\","
+                      + "\"jobStatus\":\"SUCCEEDED\","
+                      + "\"operation\":\"drain\","
+                      + "\"unknownField\":\"someValue\","
+                      + "\"startTime\":\"2026-05-12T14:30:00Z\"}";
+
+        OperationalJobResponse response = objectMapper.readValue(json, 
OperationalJobResponse.class);
+
+        
assertThat(response.status()).isEqualTo(OperationalJobStatus.SUCCEEDED);
+        
assertThat(response.startTime()).isEqualTo(Instant.parse("2026-05-12T14:30:00Z"));
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
index 9ce60c42..31ef88f4 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
@@ -26,6 +26,7 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 import javax.management.Notification;
@@ -307,6 +308,7 @@ public class CassandraAdapterDelegate implements 
ICassandraAdapter, Host.StateLi
         String partitionerName = storageOperations.getPartitionerName();
         List<String> tokens = maybeGetTokens(storageOperations);
         String dataCenter = endpointSnitchOperations.getDatacenter();
+        UUID hostId = UUID.fromString(storageOperations.getLocalHostId());
 
         return NodeSettings.builder()
                            .releaseVersion(releaseVersion)
@@ -316,6 +318,7 @@ public class CassandraAdapterDelegate implements 
ICassandraAdapter, Host.StateLi
                            .tokens(new LinkedHashSet<>(tokens))
                            
.rpcAddress(localNativeTransportAddress.getAddress())
                            .rpcPort(localNativeTransportAddress.getPort())
+                           .hostId(hostId)
                            .build();
     }
 
@@ -689,6 +692,11 @@ public class CassandraAdapterDelegate implements 
ICassandraAdapter, Host.StateLi
          * @return a collection of tokens formatted as strings
          */
         List<String> getTokens();
+
+        /**
+         * @return the local host UUID as a string
+         */
+        String getLocalHostId();
     }
 
     /**
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ListOperationalJobsHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ListOperationalJobsHandler.java
index 484039e5..ca840703 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ListOperationalJobsHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ListOperationalJobsHandler.java
@@ -35,7 +35,6 @@ import 
org.apache.cassandra.sidecar.utils.CassandraInputValidator;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.jetbrains.annotations.NotNull;
 
-import static 
org.apache.cassandra.sidecar.common.data.OperationalJobStatus.RUNNING;
 
 /**
  * Handler for retrieving the all the jobs running on the sidecar
@@ -72,7 +71,18 @@ public class ListOperationalJobsHandler extends 
AbstractHandler<Void> implements
         ListOperationalJobsResponse listResponse = new 
ListOperationalJobsResponse();
         jobManager.allInflightJobs()
                   .stream()
-                  .map(job -> new OperationalJobResponse(job.jobId(), RUNNING, 
job.name(), null))
+                  .map(job -> OperationalJobResponse.builder()
+                                                    .jobId(job.jobId())
+                                                    .status(job.status())
+                                                    .operation(job.name())
+                                                    .startTime(job.startTime())
+                                                    
.nodesPending(job.nodesPending())
+                                                    
.nodesExecuting(job.nodesExecuting())
+                                                    
.nodesSucceeded(job.nodesSucceeded())
+                                                    
.nodesFailed(job.nodesFailed())
+                                                    
.lastUpdate(job.lastUpdate())
+                                                    .build()
+                  )
                   .forEach(listResponse::addJob);
         context.json(listResponse);
     }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDecommissionHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDecommissionHandler.java
index d2c522eb..2c9bfd48 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDecommissionHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDecommissionHandler.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.handlers;
 
 import java.util.Collections;
 import java.util.Set;
+import java.util.UUID;
 
 import com.datastax.driver.core.utils.UUIDs;
 import com.google.inject.Inject;
@@ -28,6 +29,8 @@ import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.auth.authorization.Authorization;
 import io.vertx.ext.web.RoutingContext;
 import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
@@ -83,8 +86,11 @@ public class NodeDecommissionHandler extends 
AbstractHandler<Boolean> implements
                                SocketAddress remoteAddress,
                                Boolean isForce)
     {
-        StorageOperations operations = 
metadataFetcher.delegate(host).storageOperations();
-        NodeDecommissionJob job = new NodeDecommissionJob(UUIDs.timeBased(), 
operations, isForce);
+        CassandraAdapterDelegate delegate = metadataFetcher.delegate(host);
+        StorageOperations operations = delegate.storageOperations();
+        NodeSettings nodeSettings = delegate.nodeSettings();
+        UUID nodeId = nodeSettings.hostId();
+        NodeDecommissionJob job = new NodeDecommissionJob(UUIDs.timeBased(), 
nodeId, operations, isForce);
         this.jobManager.trySubmitJob(job,
                                      (completedJob, exception) ->
                                      
OperationalJobUtils.sendStatusBasedResponse(context, completedJob, exception),
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandler.java
index b0de6753..63911d24 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandler.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.handlers;
 
 import java.util.Collections;
 import java.util.Set;
+import java.util.UUID;
 
 import com.datastax.driver.core.utils.UUIDs;
 import com.google.inject.Inject;
@@ -28,6 +29,8 @@ import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.auth.authorization.Authorization;
 import io.vertx.ext.web.RoutingContext;
 import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
@@ -81,8 +84,11 @@ public class NodeDrainHandler extends AbstractHandler<Void> 
implements AccessPro
                                SocketAddress remoteAddress,
                                Void unused)
     {
-        StorageOperations operations = 
metadataFetcher.delegate(host).storageOperations();
-        NodeDrainJob job = new NodeDrainJob(UUIDs.timeBased(), operations);
+        CassandraAdapterDelegate delegate = metadataFetcher.delegate(host);
+        StorageOperations operations = delegate.storageOperations();
+        NodeSettings nodeSettings = delegate.nodeSettings();
+        UUID nodeId = nodeSettings.hostId();
+        NodeDrainJob job = new NodeDrainJob(UUIDs.timeBased(), nodeId, 
operations);
         this.jobManager.trySubmitJob(job,
                                      (completedJob, exception) ->
                                      
OperationalJobUtils.sendStatusBasedResponse(context, completedJob, exception),
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandler.java
index c7679869..73176053 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandler.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.handlers;
 
 import java.util.Collections;
 import java.util.Set;
+import java.util.UUID;
 
 import com.datastax.driver.core.utils.UUIDs;
 import com.google.inject.Inject;
@@ -32,7 +33,9 @@ import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.auth.authorization.Authorization;
 import io.vertx.ext.web.RoutingContext;
 import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.request.data.NodeMoveRequestPayload;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.common.utils.StringUtils;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
@@ -99,8 +102,11 @@ public class NodeMoveHandler extends 
AbstractHandler<String> implements AccessPr
                                SocketAddress remoteAddress,
                                String newToken)
     {
-        StorageOperations operations = 
metadataFetcher.delegate(host).storageOperations();
-        NodeMoveJob job = new NodeMoveJob(UUIDs.timeBased(), newToken, 
operations);
+        CassandraAdapterDelegate delegate = metadataFetcher.delegate(host);
+        StorageOperations operations = delegate.storageOperations();
+        NodeSettings nodeSettings = delegate.nodeSettings();
+        UUID nodeId = nodeSettings.hostId();
+        NodeMoveJob job = new NodeMoveJob(UUIDs.timeBased(), nodeId, newToken, 
operations);
         this.jobManager.trySubmitJob(job,
                                      (completedJob, exception) ->
                                      
OperationalJobUtils.sendStatusBasedResponse(context, completedJob, exception),
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDecommissionJob.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDecommissionJob.java
index 778ea450..e042200f 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDecommissionJob.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDecommissionJob.java
@@ -28,6 +28,7 @@ import io.vertx.core.Future;
 import org.apache.cassandra.sidecar.common.data.OperationalJobStatus;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link OperationalJob} to perform node decommission 
operation.
@@ -41,7 +42,12 @@ public class NodeDecommissionJob extends OperationalJob
 
     public NodeDecommissionJob(UUID jobId, StorageOperations storageOps, 
boolean isForce)
     {
-        super(jobId);
+        this(jobId, null, storageOps, isForce);
+    }
+
+    public NodeDecommissionJob(UUID jobId, @Nullable UUID nodeId, 
StorageOperations storageOps, boolean isForce)
+    {
+        super(jobId, nodeId);
         this.storageOperations = storageOps;
         this.isForce = isForce;
     }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDrainJob.java 
b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDrainJob.java
index 16fe02e2..b5b1db92 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDrainJob.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeDrainJob.java
@@ -31,6 +31,7 @@ import 
org.apache.cassandra.sidecar.common.data.OperationalJobStatus;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import 
org.apache.cassandra.sidecar.common.server.exceptions.OperationalJobException;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link OperationalJob} to perform node drain operation.
@@ -79,7 +80,12 @@ public class NodeDrainJob extends OperationalJob
 
     public NodeDrainJob(UUID jobId, StorageOperations storageOps)
     {
-        super(jobId);
+        this(jobId, null, storageOps);
+    }
+
+    public NodeDrainJob(UUID jobId, @Nullable UUID nodeId, StorageOperations 
storageOps)
+    {
+        super(jobId, nodeId);
         this.storageOperations = storageOps;
     }
 
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeMoveJob.java 
b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeMoveJob.java
index d0690095..c35f13ef 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/job/NodeMoveJob.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/job/NodeMoveJob.java
@@ -29,6 +29,7 @@ import io.vertx.core.Future;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import 
org.apache.cassandra.sidecar.common.server.exceptions.OperationalJobException;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link OperationalJob} to perform node move operation.
@@ -43,7 +44,12 @@ public class NodeMoveJob extends OperationalJob
 
     public NodeMoveJob(UUID jobId, String newToken, StorageOperations 
storageOps)
     {
-        super(jobId);
+        this(jobId, null, newToken, storageOps);
+    }
+
+    public NodeMoveJob(UUID jobId, @Nullable UUID nodeId, String newToken, 
StorageOperations storageOps)
+    {
+        super(jobId, nodeId);
         this.newToken = newToken;
         this.storageOperations = storageOps;
     }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJob.java 
b/server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJob.java
index 660f7db0..8fad806f 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJob.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJob.java
@@ -18,6 +18,9 @@
 
 package org.apache.cassandra.sidecar.job;
 
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
@@ -34,6 +37,7 @@ import 
org.apache.cassandra.sidecar.common.utils.Preconditions;
 import org.apache.cassandra.sidecar.concurrent.TaskExecutorPool;
 import org.apache.cassandra.sidecar.tasks.Task;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * An abstract class representing operational jobs that run on Cassandra
@@ -44,9 +48,17 @@ public abstract class OperationalJob implements Task<Void>
 
     // use v1 time-based uuid
     private final UUID jobId;
+    @Nullable
+    private final UUID nodeId;
 
     private final Promise<Void> executionPromise;
     private volatile boolean isExecuting = false;
+    private volatile Instant startTime;
+    private volatile Instant lastUpdate;
+    private volatile List<UUID> nodesPending;
+    private volatile List<UUID> nodesExecuting;
+    private volatile List<UUID> nodesSucceeded;
+    private volatile List<UUID> nodesFailed;
 
     /**
      * Constructs a job with a unique UUID, in Pending state
@@ -54,10 +66,26 @@ public abstract class OperationalJob implements Task<Void>
      * @param jobId UUID representing the Job to be created
      */
     protected OperationalJob(UUID jobId)
+    {
+        this(jobId, null);
+    }
+
+    /**
+     * Constructs a job with a unique UUID and an associated node identifier, 
in Pending state
+     *
+     * @param jobId  UUID representing the Job to be created
+     * @param nodeId UUID of the node this job is running on, or {@code null} 
if not applicable
+     */
+    protected OperationalJob(UUID jobId, @Nullable UUID nodeId)
     {
         Preconditions.checkArgument(jobId.version() == 1, "OperationalJob 
accepts only time-based UUID");
         this.jobId = jobId;
+        this.nodeId = nodeId;
         this.executionPromise = Promise.promise();
+        this.nodesPending = nodeId != null ? new 
ArrayList<>(Collections.singletonList(nodeId)) : Collections.emptyList();
+        this.nodesExecuting = Collections.emptyList();
+        this.nodesSucceeded = Collections.emptyList();
+        this.nodesFailed = Collections.emptyList();
     }
 
     public UUID jobId()
@@ -65,6 +93,84 @@ public abstract class OperationalJob implements Task<Void>
         return jobId;
     }
 
+    /**
+     * @return the node UUID associated with this job, or {@code null} if not 
applicable
+     */
+    public @Nullable UUID nodeId()
+    {
+        return nodeId;
+    }
+
+    /**
+     * @return the time this job started execution, or {@code null} if not yet 
started
+     */
+    public @Nullable Instant startTime()
+    {
+        return startTime;
+    }
+
+    /**
+     * @return the time of the last status update for this job, or {@code 
null} if not yet started
+     */
+    public @Nullable Instant lastUpdate()
+    {
+        return lastUpdate;
+    }
+
+    /**
+     * Updates the last update time for this job.
+     *
+     * @param lastUpdate the new last update time
+     */
+    protected void lastUpdate(Instant lastUpdate)
+    {
+        this.lastUpdate = lastUpdate;
+    }
+
+    /**
+     * @return the list of nodes pending execution for this job
+     */
+    @NotNull
+    public List<UUID> nodesPending()
+    {
+        return nodesPending;
+    }
+
+    /**
+     * @return the list of nodes currently executing this job
+     */
+    @NotNull
+    public List<UUID> nodesExecuting()
+    {
+        return nodesExecuting;
+    }
+
+    /**
+     * @return the list of nodes that have succeeded executing this job
+     */
+    @NotNull
+    public List<UUID> nodesSucceeded()
+    {
+        return nodesSucceeded;
+    }
+
+    /**
+     * @return the list of nodes that have failed executing this job
+     */
+    @NotNull
+    public List<UUID> nodesFailed()
+    {
+        return nodesFailed;
+    }
+
+    /**
+     * @return whether this job requires coordination across multiple nodes
+     */
+    public boolean requiresCoordination()
+    {
+        return false;
+    }
+
     @Override
     public final Void result()
     {
@@ -203,6 +309,13 @@ public abstract class OperationalJob implements Task<Void>
     public void execute(Promise<Void> promise)
     {
         isExecuting = true;
+        startTime = Instant.now();
+        lastUpdate = startTime;
+        if (nodeId != null)
+        {
+            nodesPending = Collections.emptyList();
+            nodesExecuting = Collections.singletonList(nodeId);
+        }
         LOGGER.info("Executing job. jobId={}", jobId);
         promise.future().onComplete(executionPromise);
         try
@@ -211,11 +324,23 @@ public abstract class OperationalJob implements Task<Void>
             internalFuture.onComplete(ar -> {
                 if (ar.succeeded())
                 {
+                    if (nodeId != null)
+                    {
+                        nodesExecuting = Collections.emptyList();
+                        nodesSucceeded = Collections.singletonList(nodeId);
+                    }
+                    lastUpdate = Instant.now();
                     promise.tryComplete();
                     LOGGER.info("Complete job execution. jobId={} status={}", 
jobId, status());
                 }
                 else
                 {
+                    if (nodeId != null)
+                    {
+                        nodesExecuting = Collections.emptyList();
+                        nodesFailed = Collections.singletonList(nodeId);
+                    }
+                    lastUpdate = Instant.now();
                     promise.tryFail(ar.cause());
                     LOGGER.error("Job execution failed. jobId={} reason={}", 
jobId, ar.cause().getMessage());
                 }
@@ -224,6 +349,12 @@ public abstract class OperationalJob implements Task<Void>
         catch (Throwable e)
         {
             OperationalJobException oje = OperationalJobException.wraps(e);
+            if (nodeId != null)
+            {
+                nodesExecuting = Collections.emptyList();
+                nodesFailed = Collections.singletonList(nodeId);
+            }
+            lastUpdate = Instant.now();
             LOGGER.error("Job execution failed. jobId={} reason={}", jobId, 
oje.getMessage(), oje);
             promise.tryFail(oje);
         }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/utils/OperationalJobUtils.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/utils/OperationalJobUtils.java
index 0edd5fd1..52d43262 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/utils/OperationalJobUtils.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/utils/OperationalJobUtils.java
@@ -54,7 +54,18 @@ public class OperationalJobUtils
             String reason = exception.getMessage();
             LOGGER.error("Conflicting job encountered. reason={}", reason);
             
context.response().setStatusCode(HttpResponseStatus.CONFLICT.code());
-            context.json(new OperationalJobResponse(job.jobId(), 
OperationalJobStatus.FAILED, job.name(), reason));
+            context.json(OperationalJobResponse.builder()
+                                               .jobId(job.jobId())
+                                               
.status(OperationalJobStatus.FAILED)
+                                               .operation(job.name())
+                                               .reason(reason)
+                                               .startTime(job.startTime())
+                                               
.nodesPending(job.nodesPending())
+                                               
.nodesExecuting(job.nodesExecuting())
+                                               
.nodesSucceeded(job.nodesSucceeded())
+                                               .nodesFailed(job.nodesFailed())
+                                               .lastUpdate(job.lastUpdate())
+                                               .build());
             return;
         }
 
@@ -74,6 +85,17 @@ public class OperationalJobUtils
         {
             reason = job.asyncResult().cause().getMessage();
         }
-        context.json(new OperationalJobResponse(job.jobId(), status, 
job.name(), reason));
+        context.json(OperationalJobResponse.builder()
+                                           .jobId(job.jobId())
+                                           .status(status)
+                                           .operation(job.name())
+                                           .reason(reason)
+                                           .startTime(job.startTime())
+                                           .nodesPending(job.nodesPending())
+                                           
.nodesExecuting(job.nodesExecuting())
+                                           
.nodesSucceeded(job.nodesSucceeded())
+                                           .nodesFailed(job.nodesFailed())
+                                           .lastUpdate(job.lastUpdate())
+                                           .build());
     }
 }
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java 
b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index 0494bbe9..5e5d308e 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -22,6 +22,7 @@ import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
@@ -218,6 +219,7 @@ public class TestModule extends AbstractModule
                                                  
.rpcAddress(InetAddress.getLoopbackAddress())
                                                  .rpcPort(6475)
                                                  
.tokens(Collections.singleton("testToken"))
+                                                 .hostId(UUID.randomUUID())
                                                  .build());
         }
         delegate.setIsNativeUp(isUp);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
index e4283280..789bd7a1 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/cluster/CassandraClientTokenRingProviderTest.java
@@ -31,6 +31,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 import com.google.common.collect.Range;
@@ -338,6 +339,7 @@ public class CassandraClientTokenRingProviderTest
                                                                                
       .partitioner("org.apache.cassandra.dht.Murmur3Partitioner")
                                                                                
       .sidecarVersion("1.0-TEST")
                                                                                
       .datacenter("DC1")
+                                                                               
       .hostId(UUID.randomUUID())
                                                                                
       .build());
         
when(instanceMetadata.delegate().version()).thenReturn(SimpleCassandraVersion.create("4.0.0.68"));
         when(instanceMetadata.delegate().metadata()).thenReturn(metadata);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/CommonTest.java 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/CommonTest.java
index 3be4d4f6..86cca506 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/handlers/CommonTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/CommonTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.sidecar.handlers;
 
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -40,6 +41,7 @@ import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
 import org.apache.cassandra.sidecar.server.Server;
@@ -118,6 +120,9 @@ public class CommonTest
             this.delegate = mock(CassandraAdapterDelegate.class);
             this.storageOperations = storageOperations;
             when(delegate.storageOperations()).thenReturn(storageOperations);
+            when(delegate.nodeSettings()).thenReturn(NodeSettings.builder()
+                                                                 
.hostId(UUID.randomUUID())
+                                                                 .build());
         }
 
         @Provides
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandlerTest.java
index 49e1604a..d65f746e 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeDrainHandlerTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.sidecar.handlers;
 
 import java.util.Collections;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -45,6 +46,7 @@ import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.response.OperationalJobResponse;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
@@ -229,7 +231,11 @@ public class NodeDrainHandlerTest
 
             CassandraAdapterDelegate delegate = 
mock(CassandraAdapterDelegate.class);
 
+            NodeSettings mockNodeSettings = NodeSettings.builder()
+                                                        
.hostId(UUID.randomUUID())
+                                                        .build();
             
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+            when(delegate.nodeSettings()).thenReturn(mockNodeSettings);
             when(instanceMetadata.delegate()).thenReturn(delegate);
 
             InstancesMetadata mockInstancesMetadata = 
mock(InstancesMetadata.class);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandlerTest.java
index 1ff8294c..e22ddf40 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NodeMoveHandlerTest.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.handlers;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -46,6 +47,7 @@ import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
 import org.apache.cassandra.sidecar.common.response.OperationalJobResponse;
 import org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
@@ -398,7 +400,11 @@ public class NodeMoveHandlerTest
 
             CassandraAdapterDelegate delegate = 
mock(CassandraAdapterDelegate.class);
 
+            NodeSettings mockNodeSettings = NodeSettings.builder()
+                                                        
.hostId(UUID.randomUUID())
+                                                        .build();
             
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+            when(delegate.nodeSettings()).thenReturn(mockNodeSettings);
             when(instanceMetadata.delegate()).thenReturn(delegate);
 
             InstancesMetadata mockInstancesMetadata = 
mock(InstancesMetadata.class);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobTest.java 
b/server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobTest.java
index f3f495b7..c047d9db 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobTest.java
@@ -170,6 +170,7 @@ class OperationalJobTest
         assertThat(future.succeeded()).isTrue();
         assertThat(job.asyncResult().succeeded()).isTrue();
         assertThat(job.status()).isEqualTo(OperationalJobStatus.SUCCEEDED);
+        assertThat(job.lastUpdate()).isNotNull();
     }
 
     @Test
@@ -204,6 +205,7 @@ class OperationalJobTest
         assertThat(failingJob.asyncResult().cause())
         .isExactlyInstanceOf(OperationalJobException.class)
         .hasMessage(msg);
+        assertThat(failingJob.lastUpdate()).isNotNull();
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to