-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Add to and from XContent to ClusterBlock and ClusterBlocks #13694
Changes from all commits
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 |
---|---|---|
|
@@ -32,19 +32,28 @@ | |
|
||
package org.opensearch.cluster.block; | ||
|
||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.opensearch.cluster.metadata.Metadata; | ||
import org.opensearch.common.Nullable; | ||
import org.opensearch.common.annotation.PublicApi; | ||
import org.opensearch.common.util.set.Sets; | ||
import org.opensearch.core.common.io.stream.StreamInput; | ||
import org.opensearch.core.common.io.stream.StreamOutput; | ||
import org.opensearch.core.common.io.stream.Writeable; | ||
import org.opensearch.core.rest.RestStatus; | ||
import org.opensearch.core.xcontent.ToXContentFragment; | ||
import org.opensearch.core.xcontent.XContentBuilder; | ||
import org.opensearch.core.xcontent.XContentParser; | ||
|
||
import java.io.IOException; | ||
import java.util.EnumSet; | ||
import java.util.Locale; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
||
import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; | ||
import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_PARAM; | ||
|
||
/** | ||
* Blocks the cluster for concurrency | ||
|
@@ -53,7 +62,21 @@ | |
*/ | ||
@PublicApi(since = "1.0.0") | ||
public class ClusterBlock implements Writeable, ToXContentFragment { | ||
|
||
private static final Logger logger = LogManager.getLogger(ClusterBlock.class); | ||
static final String KEY_UUID = "uuid"; | ||
static final String KEY_DESCRIPTION = "description"; | ||
static final String KEY_RETRYABLE = "retryable"; | ||
static final String KEY_DISABLE_STATE_PERSISTENCE = "disable_state_persistence"; | ||
static final String KEY_LEVELS = "levels"; | ||
static final String KEY_STATUS = "status"; | ||
static final String KEY_ALLOW_RELEASE_RESOURCES = "allow_release_resources"; | ||
private static final Set<String> VALID_FIELDS = Sets.newHashSet( | ||
KEY_UUID, | ||
KEY_DESCRIPTION, | ||
KEY_RETRYABLE, | ||
KEY_DISABLE_STATE_PERSISTENCE, | ||
KEY_LEVELS | ||
); | ||
private final int id; | ||
@Nullable | ||
private final String uuid; | ||
|
@@ -154,24 +177,99 @@ public boolean disableStatePersistence() { | |
|
||
@Override | ||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { | ||
Metadata.XContentContext context = Metadata.XContentContext.valueOf(params.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); | ||
builder.startObject(Integer.toString(id)); | ||
if (uuid != null) { | ||
builder.field("uuid", uuid); | ||
builder.field(KEY_UUID, uuid); | ||
} | ||
builder.field("description", description); | ||
builder.field("retryable", retryable); | ||
builder.field(KEY_DESCRIPTION, description); | ||
builder.field(KEY_RETRYABLE, retryable); | ||
if (disableStatePersistence) { | ||
builder.field("disable_state_persistence", disableStatePersistence); | ||
builder.field(KEY_DISABLE_STATE_PERSISTENCE, disableStatePersistence); | ||
} | ||
builder.startArray("levels"); | ||
builder.startArray(KEY_LEVELS); | ||
for (ClusterBlockLevel level : levels) { | ||
builder.value(level.name().toLowerCase(Locale.ROOT)); | ||
} | ||
builder.endArray(); | ||
if (context == Metadata.XContentContext.GATEWAY) { | ||
builder.field(KEY_STATUS, status); | ||
builder.field(KEY_ALLOW_RELEASE_RESOURCES, allowReleaseResources); | ||
} | ||
builder.endObject(); | ||
return builder; | ||
} | ||
|
||
public static ClusterBlock fromXContent(XContentParser parser, int id) throws IOException { | ||
String uuid = null; | ||
String description = null; | ||
boolean retryable = false; | ||
boolean disableStatePersistence = false; | ||
RestStatus status = null; | ||
boolean allowReleaseResources = false; | ||
EnumSet<ClusterBlockLevel> levels = EnumSet.noneOf(ClusterBlockLevel.class); | ||
String currentFieldName = skipBlockID(parser); | ||
XContentParser.Token token; | ||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { | ||
if (token == XContentParser.Token.FIELD_NAME) { | ||
currentFieldName = parser.currentName(); | ||
} else if (token.isValue()) { | ||
switch (Objects.requireNonNull(currentFieldName)) { | ||
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. currentFiledName will always be non null after first field. For this logic to work, currentFieldName should be reset, once the corresponding value is parsed. 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. Yeah, it will always be non Null. I think I will get rid of this nonNull check rather than resetting the field every time we parse something. |
||
case KEY_UUID: | ||
uuid = parser.text(); | ||
break; | ||
case KEY_DESCRIPTION: | ||
description = parser.text(); | ||
break; | ||
case KEY_RETRYABLE: | ||
retryable = parser.booleanValue(); | ||
break; | ||
case KEY_DISABLE_STATE_PERSISTENCE: | ||
disableStatePersistence = parser.booleanValue(); | ||
break; | ||
case KEY_STATUS: | ||
status = RestStatus.valueOf(parser.text()); | ||
break; | ||
case KEY_ALLOW_RELEASE_RESOURCES: | ||
allowReleaseResources = parser.booleanValue(); | ||
break; | ||
default: | ||
logger.warn("unknown field [{}]", currentFieldName); | ||
} | ||
} else if (token == XContentParser.Token.START_ARRAY) { | ||
if (currentFieldName.equals(KEY_LEVELS)) { | ||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { | ||
levels.add(ClusterBlockLevel.fromString(parser.text(), Locale.ROOT)); | ||
} | ||
} else { | ||
logger.warn("unknown field [{}]", currentFieldName); | ||
} | ||
} else { | ||
throw new IllegalArgumentException("unexpected token [" + token + "]"); | ||
} | ||
} | ||
return new ClusterBlock(id, uuid, description, retryable, disableStatePersistence, allowReleaseResources, status, levels); | ||
} | ||
|
||
private static String skipBlockID(XContentParser parser) throws IOException { | ||
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. Can we add explanation of why this is needed? Probably an example will help here 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 have written the fromXContent is a way that if it can parse the ClusterBlock as a XContentFragment as well as a complete Object. This specific method is added to enable the parsing ClusterBlock as a complete XContentObject like given below.
Although, we might never need to parse such object. But the UT I have written in 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. How do we differentiate between XContentObject and XConentFragment? Can we separate XContentObject parsing to a separate function and then use the above method to parse the XConentFragment inside that? 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. XContentObject has a paired start end object. While for fragment that is not true. Sure, we can separate the logic to parse the fragment to a |
||
if (parser.currentToken() == null) { | ||
shiv0408 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
parser.nextToken(); | ||
} | ||
if (parser.currentToken() == XContentParser.Token.START_OBJECT) { | ||
parser.nextToken(); | ||
if (parser.currentToken() == XContentParser.Token.FIELD_NAME) { | ||
String currentFieldName = parser.currentName(); | ||
if (VALID_FIELDS.contains(currentFieldName)) { | ||
return currentFieldName; | ||
} else { | ||
// we have hit block id, just move on | ||
parser.nextToken(); | ||
shiv0408 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
out.writeVInt(id); | ||
|
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.
Can this cause BWC failures for non-remote cluster?
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.
We are only passing GATEWAY as a context when custom metadata should be stored as part of the persistent cluster state. Don't see any reason this could cause any BWC issue.
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.
Is there a possibility of mismatch of nodes with old and new version of this code? (say OpenSearch 2.14 upgrading to 2.15). In that case, this additional fields (introduced in 2.15) can't be read by older nodes running in 2.14.
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.
The XContent we are creating is not being sent to the nodes of older version. We will just be uploading this to remote repo. In case of upgrade, we will start publication of cluster state to nodes, when all the nodes have moved to 2.15. So, this would not cause any problem imo
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.
Would it not be consumed by the data nodes from remote?