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
ExecuteScriptRequest/ThreadDumpRequest error reporting #12437
Changes from 2 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 |
---|---|---|
|
@@ -16,18 +16,18 @@ | |
|
||
package com.hazelcast.internal.management.operation; | ||
|
||
import com.hazelcast.core.HazelcastException; | ||
import com.hazelcast.internal.management.ManagementDataSerializerHook; | ||
import com.hazelcast.internal.management.ScriptEngineManagerContext; | ||
import com.hazelcast.nio.ObjectDataInput; | ||
import com.hazelcast.nio.ObjectDataOutput; | ||
import com.hazelcast.util.ExceptionUtil; | ||
|
||
import java.io.IOException; | ||
|
||
import javax.script.ScriptEngine; | ||
import javax.script.ScriptEngineManager; | ||
import javax.script.ScriptException; | ||
import java.io.IOException; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
/** | ||
* Operation to execute script on the node. | ||
|
@@ -36,37 +36,29 @@ public class ScriptExecutorOperation extends AbstractManagementOperation { | |
|
||
private String engineName; | ||
private String script; | ||
private Map<String, Object> bindings; | ||
private Object result; | ||
|
||
@SuppressWarnings("unused") | ||
public ScriptExecutorOperation() { | ||
} | ||
|
||
public ScriptExecutorOperation(String engineName, String script, Map<String, Object> bindings) { | ||
public ScriptExecutorOperation(String engineName, String script) { | ||
this.engineName = engineName; | ||
this.script = script; | ||
this.bindings = bindings; | ||
} | ||
|
||
@Override | ||
public void run() throws Exception { | ||
public void run() { | ||
ScriptEngineManager scriptEngineManager = ScriptEngineManagerContext.getScriptEngineManager(); | ||
ScriptEngine engine = scriptEngineManager.getEngineByName(engineName); | ||
if (engine == null) { | ||
throw new IllegalArgumentException("Could not find ScriptEngine named '" + engineName + "'."); | ||
} | ||
engine.put("hazelcast", getNodeEngine().getHazelcastInstance()); | ||
if (bindings != null) { | ||
Set<Map.Entry<String, Object>> entries = bindings.entrySet(); | ||
for (Map.Entry<String, Object> entry : entries) { | ||
engine.put(entry.getKey(), entry.getValue()); | ||
} | ||
} | ||
try { | ||
this.result = engine.eval(script); | ||
} catch (ScriptException e) { | ||
this.result = e.getMessage(); | ||
throw new HazelcastException(ExceptionUtil.toString(e)); | ||
} | ||
} | ||
|
||
|
@@ -79,31 +71,14 @@ public Object getResponse() { | |
protected void writeInternal(ObjectDataOutput out) throws IOException { | ||
out.writeUTF(engineName); | ||
out.writeUTF(script); | ||
if (bindings != null) { | ||
out.writeInt(bindings.size()); | ||
Set<Map.Entry<String, Object>> entries = bindings.entrySet(); | ||
for (Map.Entry<String, Object> entry : entries) { | ||
out.writeUTF(entry.getKey()); | ||
out.writeObject(entry.getValue()); | ||
} | ||
} else { | ||
out.writeInt(0); | ||
} | ||
// kept for compatibility | ||
out.writeInt(0); | ||
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. Will be surrounded by a version check and conditional serialization in 3.10 with the forward port of this PR. |
||
} | ||
|
||
@Override | ||
protected void readInternal(ObjectDataInput in) throws IOException { | ||
engineName = in.readUTF(); | ||
script = in.readUTF(); | ||
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. Here we still have to consume the |
||
int size = in.readInt(); | ||
if (size > 0) { | ||
bindings = new HashMap<String, Object>(size); | ||
for (int i = 0; i < size; i++) { | ||
String key = in.readUTF(); | ||
Object value = in.readObject(); | ||
bindings.put(key, value); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,21 +19,21 @@ | |
import com.eclipsesource.json.JsonArray; | ||
import com.eclipsesource.json.JsonObject; | ||
import com.eclipsesource.json.JsonValue; | ||
import com.hazelcast.core.Member; | ||
import com.hazelcast.internal.management.ManagementCenterService; | ||
import com.hazelcast.internal.management.operation.ScriptExecutorOperation; | ||
import com.hazelcast.nio.Address; | ||
import com.hazelcast.util.AddressUtil; | ||
import com.hazelcast.util.ExceptionUtil; | ||
import com.hazelcast.util.MapUtil; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.Future; | ||
|
||
import static com.hazelcast.util.JsonUtil.getArray; | ||
import static com.hazelcast.util.JsonUtil.getBoolean; | ||
import static com.hazelcast.util.JsonUtil.getString; | ||
|
||
/** | ||
|
@@ -44,26 +44,14 @@ public class ExecuteScriptRequest implements ConsoleRequest { | |
private String script; | ||
private String engine; | ||
private Set<String> targets; | ||
private boolean targetAllMembers; | ||
private Map<String, Object> bindings; | ||
|
||
public ExecuteScriptRequest() { | ||
} | ||
|
||
public ExecuteScriptRequest(String script, String engine, boolean targetAllMembers, Map<String, Object> bindings) { | ||
public ExecuteScriptRequest(String script, String engine, Set<String> targets) { | ||
this.script = script; | ||
this.engine = engine; | ||
this.targets = new HashSet<String>(0); | ||
this.targetAllMembers = targetAllMembers; | ||
this.bindings = bindings; | ||
} | ||
|
||
public ExecuteScriptRequest(String script, String engine, Set<String> targets, Map<String, Object> bindings) { | ||
this.script = script; | ||
this.targets = targets; | ||
this.engine = engine; | ||
this.targetAllMembers = false; | ||
this.bindings = bindings; | ||
} | ||
|
||
@Override | ||
|
@@ -73,47 +61,53 @@ public int getType() { | |
|
||
@Override | ||
public void writeResponse(ManagementCenterService mcs, JsonObject root) throws Exception { | ||
JsonObject jsonResult = new JsonObject(); | ||
ArrayList results; | ||
if (targetAllMembers) { | ||
Set<Member> members = mcs.getHazelcastInstance().getCluster().getMembers(); | ||
ArrayList<Object> list = new ArrayList<Object>(members.size()); | ||
for (Member member : members) { | ||
list.add(mcs.callOnMember(member, new ScriptExecutorOperation(engine, script, bindings))); | ||
} | ||
results = list; | ||
} else { | ||
ArrayList<Object> list = new ArrayList<Object>(targets.size()); | ||
for (String address : targets) { | ||
AddressUtil.AddressHolder addressHolder = AddressUtil.getAddressHolder(address); | ||
Address targetAddress = new Address(addressHolder.getAddress(), addressHolder.getPort()); | ||
list.add(mcs.callOnAddress(targetAddress, new ScriptExecutorOperation(engine, script, bindings))); | ||
} | ||
results = list; | ||
Map<Address, Future<Object>> futures = MapUtil.createHashMap(targets.size()); | ||
|
||
for (String address : targets) { | ||
AddressUtil.AddressHolder addressHolder = AddressUtil.getAddressHolder(address); | ||
Address targetAddress = new Address(addressHolder.getAddress(), addressHolder.getPort()); | ||
futures.put(targetAddress, mcs.callOnAddress(targetAddress, new ScriptExecutorOperation(engine, script))); | ||
} | ||
|
||
StringBuilder sb = new StringBuilder(); | ||
for (Object result : results) { | ||
if (result instanceof String) { | ||
sb.append(result); | ||
} else if (result instanceof List) { | ||
List list = (List) result; | ||
for (Object o : list) { | ||
sb.append(o).append("\n"); | ||
} | ||
} else if (result instanceof Map) { | ||
Map map = (Map) result; | ||
for (Object o : map.entrySet()) { | ||
Map.Entry entry = (Map.Entry) o; | ||
sb.append(entry.getKey()).append("->").append(entry.getValue()).append("\n"); | ||
JsonObject responseJson = new JsonObject(); | ||
StringBuilder scriptResult = new StringBuilder(); | ||
for (Map.Entry<Address, Future<Object>> entry : futures.entrySet()) { | ||
Address address = entry.getKey(); | ||
Future<Object> future = entry.getValue(); | ||
|
||
try { | ||
Object result = future.get(); | ||
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 the threading/async resolving and its error handling should not be intertwined with composing a script 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 went back and forth between creating a custom class for returning the call's success/failure output together with its result/error message (since Java doesn't allow multiple return values from a method) and leaving it to 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. Not necessarily - I just mean composing a string and error handling required because of |
||
|
||
StringBuilder sb = new StringBuilder(); | ||
if (result instanceof String) { | ||
sb.append(result); | ||
} else if (result instanceof List) { | ||
List list = (List) result; | ||
for (Object o : list) { | ||
sb.append(o).append("\n"); | ||
} | ||
} else if (result instanceof Map) { | ||
Map map = (Map) result; | ||
for (Object o : map.entrySet()) { | ||
Map.Entry e = (Map.Entry) o; | ||
sb.append(e.getKey()).append("->").append(e.getValue()).append("\n"); | ||
} | ||
} else if (result == null) { | ||
sb.append("error"); | ||
} | ||
} else if (result == null) { | ||
sb.append("error"); | ||
sb.append("\n"); | ||
|
||
addSuccessResponse(responseJson, scriptResult, address, sb.toString()); | ||
} catch (ExecutionException e) { | ||
addErrorResponse(responseJson, scriptResult, address, e); | ||
} catch (InterruptedException e) { | ||
addErrorResponse(responseJson, scriptResult, address, e); | ||
Thread.currentThread().interrupt(); | ||
} | ||
sb.append("\n"); | ||
} | ||
jsonResult.add("scriptResult", sb.toString()); | ||
root.add("result", jsonResult); | ||
|
||
responseJson.add("scriptResult", scriptResult.toString()); | ||
root.add("result", responseJson); | ||
} | ||
|
||
@Override | ||
|
@@ -124,7 +118,27 @@ public void fromJson(JsonObject json) { | |
for (JsonValue target : getArray(json, "targets", new JsonArray())) { | ||
targets.add(target.asString()); | ||
} | ||
targetAllMembers = getBoolean(json, "targetAllMembers", false); | ||
bindings = new HashMap<String, Object>(); | ||
} | ||
|
||
private static void addSuccessResponse(JsonObject root, StringBuilder scriptResult, | ||
Address address, String result) { | ||
|
||
addResponse(root, scriptResult, address, true, result); | ||
} | ||
|
||
private static void addErrorResponse(JsonObject root, StringBuilder scriptResult, | ||
Address address, Exception e) { | ||
addResponse(root, scriptResult, address, false, ExceptionUtil.toString(e)); | ||
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. Do we have to append all the exception results to the 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 we should still have the full stacktrace, otherwise we wouldn't be able to show the exception details in logs either (which would limit usability of the scripting tab). As this is a security fix, we can always ask the user to upgrade to latest to have full security. I have addressed all other comments. Thanks for the review. |
||
} | ||
|
||
private static void addResponse(JsonObject root, StringBuilder scriptResult, | ||
Address address, boolean success, String result) { | ||
|
||
JsonObject json = new JsonObject(); | ||
json.add("success", success); | ||
json.add("result", result); | ||
root.add(address.toString(), json); | ||
|
||
scriptResult.append(result); | ||
} | ||
} |
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.
I 'd rather keep the
Operation
construction on a separate line for readability.