Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract variable api from ScriptSession, let ScriptSession guard reads #4970

Merged
merged 27 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
afcddf7
Extract variable api from ScriptSession, let ScriptSession guard reads
niloc132 Dec 20, 2023
4e2beee
Further shift from script session to query scope, including liveness
niloc132 Dec 20, 2023
7041746
Restore method needed for python local scope hack
niloc132 Dec 20, 2023
06ce7f0
Fix test setup, correct unwrapping in sql
niloc132 Dec 21, 2023
7b57fa2
Partial review feedback
niloc132 Dec 21, 2023
5a1a7cf
Remove VariableProvider, use QueryScope instead
niloc132 Dec 21, 2023
d309e35
Remove dead method, dead cleanup code in flight round trip test
niloc132 Dec 21, 2023
8b7c820
Remove more dead code, fix potential lock order issue
niloc132 Dec 21, 2023
bd2abcc
Field ordering
niloc132 Dec 21, 2023
8f8102e
Correct comment layout
niloc132 Dec 21, 2023
4efb27e
Review feedback:
niloc132 Dec 22, 2023
c049476
Revert attempt to use optional (incompatible with null returns)
niloc132 Dec 22, 2023
ff6a3ba
Merge branch 'main' into 4040-cleanup-5
niloc132 Jan 8, 2024
deb0a77
Review feedback
niloc132 Jan 8, 2024
c5c1ce7
Old requested rename, and added missing transferTo
niloc132 Jan 8, 2024
42ba9be
Finish implementation (don't trust IJ's refactor tools..?)
niloc132 Jan 8, 2024
0b6caa8
Review feedback
niloc132 Jan 9, 2024
d3f0472
Merge branch 'main' into 4040-cleanup-5
niloc132 Jan 9, 2024
e3a0583
Merge branch 'main' into 4040-cleanup-5
niloc132 Jan 9, 2024
cb8646d
Live code review
niloc132 Jan 11, 2024
abb9dcb
Live review feedback
niloc132 Jan 12, 2024
25e4da7
Clean up standalone impl
niloc132 Jan 12, 2024
7843871
Merge branch 'main' into 4040-cleanup-5
niloc132 Jan 12, 2024
d2208c3
spotless
niloc132 Jan 12, 2024
7598e14
Review feedback
niloc132 Jan 18, 2024
668596d
Review feedback
niloc132 Jan 19, 2024
1e32a0e
Add missing queryScope.unwrapObject() calls
niloc132 Jan 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
import io.deephaven.plugin.type.ObjectTypeLookup.NoOp;
import io.deephaven.util.SafeCloseable;
import io.deephaven.util.annotations.ScriptApi;
import io.deephaven.util.annotations.VisibleForTesting;
import io.deephaven.util.thread.NamingThreadFactory;
import io.deephaven.util.thread.ThreadInitializationFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jpy.KeyError;
import org.jpy.PyDictWrapper;
import org.jpy.PyInputMode;
import org.jpy.PyLib;
import org.jpy.PyLib.CallableKind;
import org.jpy.PyModule;
import org.jpy.PyObject;
Expand All @@ -41,14 +41,13 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -150,13 +149,6 @@ public Thread newThread(@NotNull Runnable r) {
}
}

@Override
@VisibleForTesting
public QueryScope newQueryScope() {
// depend on the GIL instead of local synchronization
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
return new UnsynchronizedScriptSessionQueryScope(this);
}

/**
* Finds the specified script; and runs it as a file, or if it is a stream writes it to a temporary file in order to
* run it.
Expand All @@ -183,20 +175,14 @@ private void runScript(String script) throws IOException {
}
}

@NotNull
@SuppressWarnings("unchecked")
@Override
public Object getVariable(String name) throws QueryScope.MissingVariableException {
return scope
protected <T> T getVariable(String name) throws QueryScope.MissingVariableException {
return (T) scope
.getValue(name)
.orElseThrow(() -> new QueryScope.MissingVariableException("No variable for: " + name));
.orElseThrow(() -> new QueryScope.MissingVariableException("Missing variable " + name));
}

@Override
public <T> T getVariable(String name, T defaultValue) {
return scope
.<T>getValueUnchecked(name)
.orElse(defaultValue);
}

@SuppressWarnings("unused")
@ScriptApi
Expand Down Expand Up @@ -224,13 +210,6 @@ protected void evaluate(String command, String scriptName) {
}
}

@Override
public Map<String, Object> getVariables() {
final Map<String, Object> outMap = new LinkedHashMap<>();
scope.getEntriesMap().forEach((key, value) -> outMap.put(key, maybeUnwrap(value)));
return outMap;
}

protected static class PythonSnapshot implements Snapshot, SafeCloseable {

private final PyDictWrapper dict;
Expand Down Expand Up @@ -295,34 +274,49 @@ private Object maybeUnwrap(PyObject o) {
}

@Override
public Set<String> getVariableNames() {
return Collections.unmodifiableSet(scope.getKeys().collect(Collectors.toSet()));
protected Set<String> getVariableNames(Predicate<String> allowName) {
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
return PyLib.ensureGil(() -> scope.getKeys().filter(allowName).collect(Collectors.toUnmodifiableSet()));
}

@Override
public boolean hasVariableName(String name) {
protected boolean hasVariable(String name) {
return scope.containsKey(name);
}

@Override
public synchronized void setVariable(String name, @Nullable Object newValue) {
final PyDictWrapper globals = scope.mainGlobals();
if (newValue == null) {
try {
globals.delItem(name);
} catch (KeyError key) {
// ignore
}
} else {
if (!(newValue instanceof PyObject)) {
newValue = PythonObjectWrapper.wrap(newValue);
protected synchronized Object setVariable(String name, @Nullable Object newValue) {
Object old = PyLib.ensureGil(() -> {
final PyDictWrapper globals = scope.mainGlobals();

if (newValue == null) {
try {
return globals.unwrap().callMethod("pop", name);
} catch (KeyError key) {
return null;
}
} else {
Object wrapped;
if (newValue instanceof PyObject) {
wrapped = newValue;
} else {
wrapped = PythonObjectWrapper.wrap(newValue);
}
// This isn't thread safe, we're relying on the GIL being kind to us (as we have historically done).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true, given that we've enclosing with ensureGil?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically no, the GIL doesn't guarantee us practically anything even if this was one call instead of two - __setitem__ is just a function that could be redefined in python in a way that lets it drop the gil while invoking it...

Practically, we hope that it continued to be kind to us.

// There
// is no built-in for "replace a variable and return the old one".
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
Object prev = globals.get(name);
globals.setItem(name, wrapped);
return prev;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be unwrapping prev?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we require callers to invoke unwrap if they want it unwrapped, we don't do it for them - this is consistent with other calls.

}
globals.setItem(name, newValue);
}
});

// Observe changes from this "setVariable" (potentially capturing previous or concurrent external changes from
// other threads)
observeScopeChanges();

// This doesn't return the same Java instance of PyObject, so we won't decref it properly, but
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make sure I understand this, let's discuss.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call ScriptSessionQueryScope.putParam("foo", somePyObjectInstance), and then call same = readParamValue("foo") to get it back - the PyObject instances are pointing at the "same" python object, but same == somePyObjectInstance is false - they are separate java object instances, and contain separate borrowed pointers into py/c.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the solution? Is @jmao-denver aware?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't a great solution in general, except to cleanly close PyObject basically everywhere - java gc is required to mop up all the unclosed PyObject. This is wrong in enough places in DHC that it isn't worth dedicating effort to it here.

// again, that is consistent with how we've historically treated these references.
return old;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,4 @@ public <T> T readParamValue(String name, T defaultValue) {
public <T> void putParam(String name, T value) {
throw new IllegalStateException("EmptyQueryScope cannot create parameters");
}

@Override
public void putObjectFields(Object object) {
throw new IllegalStateException("EmptyQueryScope cannot create parameters");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ public ExecutionContext withOperationInitializer(final OperationInitializer oper
operationInitializer);
}

/**
* Returns, or creates, an execution context with the given value for {@code queryScope} and existing values for the
* other members.
*
* @param queryScope the query scope to use instead
* @return the execution context
*/
public ExecutionContext withQueryScope(QueryScope queryScope) {
if (queryScope == this.queryScope) {
return this;
}
return new ExecutionContext(isSystemic, authContext, queryLibrary, queryScope, queryCompiler, updateGraph,
operationInitializer);
}


/**
* Execute runnable within this execution context.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,4 @@ public <T> T readParamValue(String name, T defaultValue) {
public <T> void putParam(String name, T value) {
fail();
}

@Override
public void putObjectFields(Object object) {
fail();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
*/
package io.deephaven.engine.context;

import io.deephaven.engine.liveness.Liveness;
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
import io.deephaven.engine.liveness.LivenessReferent;
import io.deephaven.engine.liveness.ReferenceCountedLivenessNode;
import io.deephaven.engine.updategraph.DynamicNode;
import io.deephaven.time.DateTimeUtils;
import io.deephaven.hash.KeyedObjectHashMap;
import io.deephaven.hash.KeyedObjectKey;
Expand All @@ -21,7 +25,7 @@
/**
* Variable scope used to resolve parameter values during query execution.
*/
public abstract class QueryScope implements LogOutputAppendable {
public abstract class QueryScope extends ReferenceCountedLivenessNode implements LogOutputAppendable {
rcaudy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Adds a parameter to the default instance {@link QueryScope}, or updates the value of an existing parameter.
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -34,15 +38,6 @@ public static <T> void addParam(final String name, final T value) {
ExecutionContext.getContext().getQueryScope().putParam(name, value);
}

/**
* Adds an object's declared fields to the scope.
*
* @param object object whose fields will be added.
*/
public static void addObjectFields(final Object object) {
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
ExecutionContext.getContext().getQueryScope().putObjectFields(object);
}

/**
* Gets a parameter from the default instance {@link QueryScope}.
*
Expand Down Expand Up @@ -119,6 +114,12 @@ private static Object applyValueConversions(final Object value) {
return value;
}

protected QueryScope() {
super(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the only things we intend to manage are things we already hold strongly, maybe we should actually use enforceStrongReachability=true. That results in a simpler/cleaner implementation of reference tracking. On the other hand, if we expect query scopes to be leaked (as in garbage), that may not be what we want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question could now apply to ScriptSessionQueryScope, do you still want to consider it?


retainReference();
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
}

// -----------------------------------------------------------------------------------------------------------------
// Scope manipulation helper methods
// -----------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -194,12 +195,15 @@ public final QueryScopeParam[] getParams(final Collection<String> names) throws
public abstract <T> void putParam(final String name, final T value);

/**
* Add an object's public members (referenced reflectively, not a shallow copy!) to this scope if supported.
* <b>Note:</b> This is an optional method.
* Asks the session to remove any wrapping that exists on scoped objects so that clients can fetch them. Defaults to
niloc132 marked this conversation as resolved.
Show resolved Hide resolved
* returning the object itself.
*
* @param object object to add public members from.
* @param object the scoped object
* @return an obj which can be consumed by a client
*/
public abstract void putObjectFields(final Object object);
public Object unwrapObject(Object object) {
return object;
}

// -----------------------------------------------------------------------------------------------------------------
// LogOutputAppendable implementation
Expand Down Expand Up @@ -232,7 +236,11 @@ public static class StandaloneImpl extends QueryScope {
private final KeyedObjectHashMap<String, ValueRetriever> valueRetrievers =
new KeyedObjectHashMap<>(new ValueRetrieverNameKey());

public StandaloneImpl() {}
@Override
protected void destroy() {
super.destroy();
valueRetrievers.clear();
}
niloc132 marked this conversation as resolved.
Show resolved Hide resolved

@Override
public Set<String> getParamNames() {
Expand Down Expand Up @@ -276,14 +284,19 @@ public <T> T readParamValue(final String name, final T defaultValue) {

@Override
public <T> void putParam(final String name, final T value) {
if (value instanceof LivenessReferent && DynamicNode.notDynamicOrIsRefreshing(value)) {
rcaudy marked this conversation as resolved.
Show resolved Hide resolved
manage((LivenessReferent) value);
}
NameValidator.validateQueryParameterName(name);
// TODO: Can I get rid of this applyValueConversions? It's too inconsistent to feel safe.
valueRetrievers.put(name, new SimpleValueRetriever<>(name, applyValueConversions(value)));
}
ValueRetriever<?> oldValueRetriever =
valueRetrievers.put(name, new SimpleValueRetriever<>(name, applyValueConversions(value)));

public void putObjectFields(final Object object) {
for (final Field field : object.getClass().getDeclaredFields()) {
valueRetrievers.put(field.getName(), new ReflectiveValueRetriever(object, field));
if (oldValueRetriever != null) {
Object oldValue = oldValueRetriever.getValue();
if (oldValue instanceof LivenessReferent && DynamicNode.notDynamicOrIsRefreshing(oldValue)) {
unmanage((LivenessReferent) oldValue);
}
}
}

Expand Down
15 changes: 6 additions & 9 deletions engine/sql/src/main/java/io/deephaven/engine/sql/Sql.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutput.ObjFormatter;
import io.deephaven.engine.context.ExecutionContext;
import io.deephaven.engine.context.QueryScope;
import io.deephaven.engine.table.ColumnDefinition;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableDefinition;
import io.deephaven.engine.table.impl.TableCreatorImpl;
import io.deephaven.engine.util.AbstractScriptSession.ScriptSessionQueryScope;
import io.deephaven.engine.util.ScriptSession;
import io.deephaven.internal.log.LoggerFactory;
import io.deephaven.io.logger.Logger;
import io.deephaven.qst.column.header.ColumnHeader;
Expand Down Expand Up @@ -83,18 +82,16 @@ private static Map<String, Table> currentScriptSessionNamedTables() {
final Map<String, Table> scope = new HashMap<>();
// getVariables() is inefficient
// See SQLTODO(catalog-reader-implementation)
for (Entry<String, Object> e : currentScriptSession().getVariables().entrySet()) {
if (e.getValue() instanceof Table) {
scope.put(e.getKey(), (Table) e.getValue());
QueryScope queryScope = ExecutionContext.getContext().getQueryScope();
for (String name : queryScope.getParamNames()) {
Object paramValue = queryScope.unwrapObject(queryScope.readParamValue(name));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a lock on the QueryScope to make this safe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the unwrapping safe? At least not for the implementation (only python) we have today, no.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, to make the readParamValue results be consistent with getParamNames. Otherwise, you could have a MissingVariableException due to concurrent mutation of the QueryScope.
Feels like a missing method to get values, to be honest.

if (paramValue instanceof Table) {
scope.put(name, (Table) paramValue);
}
}
return scope;
}

private static ScriptSession currentScriptSession() {
return ((ScriptSessionQueryScope) ExecutionContext.getContext().getQueryScope()).scriptSession();
}

private static TableHeader adapt(TableDefinition tableDef) {
final Builder builder = TableHeader.builder();
for (ColumnDefinition<?> cd : tableDef.getColumns()) {
Expand Down
Loading
Loading