-
Notifications
You must be signed in to change notification settings - Fork 52
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
Reaper endpoints: Async Repair Endpoint #358
Changes from 15 commits
61d14c2
ea1c395
d82219b
fa342ab
5203111
da8b860
660c40f
afc5f3a
55bce6b
2dfe4aa
1a5d555
72f1cc0
f29c16b
10b460c
16f701a
34090f3
7622eca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import com.datastax.mgmtapi.rpc.Rpc; | ||
import com.datastax.mgmtapi.rpc.RpcParam; | ||
import com.datastax.mgmtapi.rpc.RpcRegistry; | ||
import com.datastax.mgmtapi.rpc.models.RingRange; | ||
import com.datastax.mgmtapi.util.Job; | ||
import com.datastax.mgmtapi.util.JobExecutor; | ||
import com.datastax.oss.driver.api.core.CqlIdentifier; | ||
|
@@ -34,6 +35,7 @@ | |
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ExecutionException; | ||
|
@@ -54,6 +56,7 @@ | |
import org.apache.cassandra.service.StorageProxy; | ||
import org.apache.cassandra.utils.Pair; | ||
import org.apache.cassandra.utils.progress.ProgressEventType; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
|
@@ -737,98 +740,106 @@ public void clearSnapshots( | |
@Rpc(name = "repair") | ||
public String repair( | ||
@RpcParam(name = "keyspaceName") String keyspace, | ||
@RpcParam(name = "tables") List<String> tables, | ||
@RpcParam(name = "tables") Optional<List<String>> tables, | ||
@RpcParam(name = "full") Boolean full, | ||
@RpcParam(name = "notifications") boolean notifications) | ||
@RpcParam(name = "notifications") boolean notifications, | ||
@RpcParam(name = "repairParallelism") Optional<RepairParallelism> repairParallelism, | ||
@RpcParam(name = "datacenters") Optional<Collection<String>> datacenters, | ||
Miles-Garnsey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@RpcParam(name = "associatedTokens") Optional<List<RingRange>> associatedTokens, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issue: |
||
@RpcParam(name = "repairThreadCount") Optional<Integer> repairThreadCount) | ||
throws IOException { | ||
// At least one keyspace is required | ||
if (keyspace != null) { | ||
// create the repair spec | ||
Map<String, String> repairSpec = new HashMap<>(); | ||
|
||
// add any specified tables to the repair spec | ||
if (tables != null && !tables.isEmpty()) { | ||
// set the tables/column families | ||
repairSpec.put(RepairOption.COLUMNFAMILIES_KEY, String.join(",", tables)); | ||
} | ||
|
||
// handle incremental vs full | ||
boolean isIncremental = Boolean.FALSE.equals(full); | ||
repairSpec.put(RepairOption.INCREMENTAL_KEY, Boolean.toString(isIncremental)); | ||
if (isIncremental) { | ||
// incremental repairs will fail if parallelism is not set | ||
repairSpec.put(RepairOption.PARALLELISM_KEY, RepairParallelism.PARALLEL.getName()); | ||
} | ||
|
||
// Since Cassandra provides us with a async, we don't need to use our executor interface for | ||
// this. | ||
final int repairJobId = | ||
ShimLoader.instance.get().getStorageService().repairAsync(keyspace, repairSpec); | ||
|
||
if (!notifications) { | ||
return Integer.valueOf(repairJobId).toString(); | ||
} | ||
assert (keyspace != null); | ||
Map<String, String> repairSpec = new HashMap<>(); | ||
repairParallelism.map(rPar -> repairSpec.put(RepairOption.PARALLELISM_KEY, rPar.getName())); | ||
repairSpec.put(RepairOption.INCREMENTAL_KEY, Boolean.toString(!full)); | ||
repairThreadCount.map( | ||
tCount -> | ||
repairSpec.put( | ||
RepairOption.JOB_THREADS_KEY, Integer.toString(tCount == 0 ? 1 : tCount))); | ||
repairSpec.put(RepairOption.TRACE_KEY, Boolean.toString(Boolean.FALSE)); | ||
tables.map( | ||
tabs -> repairSpec.put(RepairOption.COLUMNFAMILIES_KEY, StringUtils.join(tables, ","))); | ||
if (full) { | ||
associatedTokens.map( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issue: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think input sanitization should be handled in the Resources layer, no? By the time it gets to the NodeOpsProvider I'd rather have everything in Optionals, but tell me if you disagree. I do wonder if introducing Optionals here just adds more complexity as now we have a third type of nullity. |
||
aTokens -> | ||
repairSpec.put( | ||
RepairOption.RANGES_KEY, | ||
StringUtils.join( | ||
aTokens.stream() | ||
.map(token -> token.getStart() + ":" + token.getEnd()) | ||
.collect(Collectors.toList()), | ||
","))); | ||
} | ||
datacenters.map( | ||
dcs -> repairSpec.put(RepairOption.DATACENTERS_KEY, StringUtils.join(dcs, ","))); | ||
|
||
String jobId = String.format("repair-%d", repairJobId); | ||
final Job job = service.createJob("repair", jobId); | ||
// Since Cassandra provides us with a async, we don't need to use our executor interface for | ||
// this. | ||
final int repairJobId = | ||
ShimLoader.instance.get().getStorageService().repairAsync(keyspace, repairSpec); | ||
|
||
if (repairJobId == 0) { | ||
// Job is done and won't continue | ||
job.setStatusChange(ProgressEventType.COMPLETE, ""); | ||
job.setStatus(Job.JobStatus.COMPLETED); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
service.updateJob(job); | ||
return job.getJobId(); | ||
} | ||
if (!notifications) { | ||
return Integer.valueOf(repairJobId).toString(); | ||
} | ||
|
||
ShimLoader.instance | ||
.get() | ||
.getStorageService() | ||
.addNotificationListener( | ||
(notification, handback) -> { | ||
if (notification.getType().equals("progress")) { | ||
Map<String, Integer> data = (Map<String, Integer>) notification.getUserData(); | ||
ProgressEventType progress = ProgressEventType.values()[data.get("type")]; | ||
|
||
switch (progress) { | ||
case START: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
job.setStartTime(System.currentTimeMillis()); | ||
break; | ||
case NOTIFICATION: | ||
case PROGRESS: | ||
break; | ||
case ERROR: | ||
case ABORT: | ||
job.setError(new RuntimeException(notification.getMessage())); | ||
job.setStatus(Job.JobStatus.ERROR); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
break; | ||
case SUCCESS: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
// SUCCESS / ERROR does not mean the job has completed yet (COMPLETE is that) | ||
break; | ||
case COMPLETE: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
job.setStatus(Job.JobStatus.COMPLETED); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
break; | ||
} | ||
service.updateJob(job); | ||
} | ||
}, | ||
(NotificationFilter) | ||
notification -> { | ||
final int repairNo = | ||
Integer.parseInt(((String) notification.getSource()).split(":")[1]); | ||
return repairNo == repairJobId; | ||
}, | ||
null); | ||
String jobId = String.format("repair-%d", repairJobId); | ||
final Job job = service.createJob("repair", jobId); | ||
|
||
if (repairJobId == 0) { | ||
// Job is done and won't continue | ||
job.setStatusChange(ProgressEventType.COMPLETE, ""); | ||
job.setStatus(Job.JobStatus.COMPLETED); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
service.updateJob(job); | ||
return job.getJobId(); | ||
} | ||
|
||
throw new RuntimeException("At least one keyspace must be defined"); | ||
ShimLoader.instance | ||
.get() | ||
.getStorageService() | ||
.addNotificationListener( | ||
(notification, handback) -> { | ||
if (notification.getType().equals("progress")) { | ||
Map<String, Integer> data = (Map<String, Integer>) notification.getUserData(); | ||
ProgressEventType progress = ProgressEventType.values()[data.get("type")]; | ||
|
||
switch (progress) { | ||
case START: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
job.setStartTime(System.currentTimeMillis()); | ||
break; | ||
case NOTIFICATION: | ||
case PROGRESS: | ||
break; | ||
case ERROR: | ||
case ABORT: | ||
job.setError(new RuntimeException(notification.getMessage())); | ||
job.setStatus(Job.JobStatus.ERROR); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
break; | ||
case SUCCESS: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
// SUCCESS / ERROR does not mean the job has completed yet (COMPLETE is that) | ||
break; | ||
case COMPLETE: | ||
job.setStatusChange(progress, notification.getMessage()); | ||
job.setStatus(Job.JobStatus.COMPLETED); | ||
job.setFinishedTime(System.currentTimeMillis()); | ||
break; | ||
} | ||
service.updateJob(job); | ||
} | ||
}, | ||
(NotificationFilter) | ||
notification -> { | ||
final int repairNo = | ||
Integer.parseInt(((String) notification.getSource()).split(":")[1]); | ||
return repairNo == repairJobId; | ||
}, | ||
null); | ||
|
||
return job.getJobId(); | ||
} | ||
|
||
@Rpc(name = "move") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright DataStax, Inc. | ||
* | ||
* Please see the included license file for details. | ||
*/ | ||
|
||
package com.datastax.mgmtapi.rpc.models; | ||
|
||
import java.math.BigInteger; | ||
import java.util.Comparator; | ||
|
||
public final class RingRange { | ||
|
||
public static final Comparator<RingRange> START_COMPARATOR = | ||
(RingRange o1, RingRange o2) -> o1.start.compareTo(o2.start); | ||
|
||
public final BigInteger start; | ||
public final BigInteger end; | ||
|
||
public RingRange(BigInteger start, BigInteger end) { | ||
this.start = start; | ||
this.end = end; | ||
} | ||
|
||
public RingRange(String... range) { | ||
start = new BigInteger(range[0]); | ||
end = new BigInteger(range[1]); | ||
} | ||
|
||
public BigInteger getStart() { | ||
return start; | ||
} | ||
|
||
public BigInteger getEnd() { | ||
return end; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue:
There are 2 issues here:
RepairParallelism
andOptional
are not included in the variouscom.datastax.mgmtapi.rpc.GenericSerializer
classesRepairParallelism
is a class that exists in Cassandra (org.apache.cassandra.repair.RepairParallelism
) and in the Management API server code (com.datastax.mgmtapi.resources.v2.models.RepairParallelism
)We could add the classes to the GenericSerializers, but that's a slippery slope. Right now, it supports Java primitives and basic collection types.
The second issue is more of a confusing thing. The v2 model object looks a lot like the Cassandra object, but they are not the same. And in the Resource code, we are not sending the Cassandra object, we are sending the V2 model object. So this will never deserialize correctly since we are expecting the Cassandra object in this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woops... Sorry, I should have spotted that one :(
Wouldn't we be better off adding a generic serializer for the collections interface to make things more generic? I have to be honest, I do not like the way management API is not using the type system very much throughout. I understand some of that is unavoidable due to the way the RPC calls work, but if we can't even serialise our own types, that is unfortunate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I need to understand why we aren't just serializing straight to json or using some off the shelf serialization here - was there a reason for writing our own serializer?