Skip to content

Commit 2075615

Browse files
authored
feat: Add add_async/delete_async methods in InputTable (deephaven#6061)
Fixes deephaven#3887
1 parent dcd7fc5 commit 2075615

File tree

4 files changed

+215
-27
lines changed

4 files changed

+215
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
3+
//
4+
package io.deephaven.integrations.python;
5+
6+
import io.deephaven.engine.util.input.InputTableStatusListener;
7+
import io.deephaven.internal.log.LoggerFactory;
8+
import io.deephaven.io.logger.Logger;
9+
import io.deephaven.util.annotations.ScriptApi;
10+
import org.apache.commons.lang3.exception.ExceptionUtils;
11+
import org.jetbrains.annotations.NotNull;
12+
import org.jetbrains.annotations.Nullable;
13+
import org.jpy.PyObject;
14+
15+
import java.util.Objects;
16+
17+
@ScriptApi
18+
public class PythonInputTableStatusListenerAdapter implements InputTableStatusListener {
19+
20+
private static final Logger log = LoggerFactory.getLogger(PythonInputTableStatusListenerAdapter.class);
21+
private final PyObject pyOnSuccessCallback;
22+
private final PyObject pyOnErrorCallback;
23+
24+
/**
25+
* Create a Python InputTable status listener.
26+
*
27+
* @param pyOnSuccessCallback The Python onSuccess callback function.
28+
* @param pyOnErrorCallback The Python onError callback function.
29+
*/
30+
private PythonInputTableStatusListenerAdapter(@Nullable PyObject pyOnSuccessCallback,
31+
@NotNull PyObject pyOnErrorCallback) {
32+
this.pyOnSuccessCallback = pyOnSuccessCallback;
33+
this.pyOnErrorCallback = pyOnErrorCallback;
34+
}
35+
36+
public static PythonInputTableStatusListenerAdapter create(@Nullable PyObject pyOnSuccessCallback,
37+
@NotNull PyObject pyOnErrorCallback) {
38+
return new PythonInputTableStatusListenerAdapter(pyOnSuccessCallback,
39+
Objects.requireNonNull(pyOnErrorCallback, "Python on_error callback cannot be None"));
40+
}
41+
42+
@Override
43+
public void onError(Throwable originalException) {
44+
try {
45+
pyOnErrorCallback.call("__call__", ExceptionUtils.getStackTrace(originalException));
46+
} catch (Throwable e) {
47+
// If the Python onFailure callback fails, log the new exception
48+
// and continue with the original exception.
49+
log.error().append("Python on_error callback failed: ").append(e).endl();
50+
}
51+
}
52+
53+
@Override
54+
public void onSuccess() {
55+
if (pyOnSuccessCallback != null && !pyOnSuccessCallback.isNone()) {
56+
pyOnSuccessCallback.call("__call__");
57+
} else {
58+
InputTableStatusListener.super.onSuccess();
59+
}
60+
}
61+
}

py/server/deephaven/table_factory.py

+97-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
#
22
# Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
33
#
4-
54
""" This module provides various ways to make a Deephaven table. """
6-
5+
from functools import wraps
76
from typing import Callable, List, Dict, Any, Union, Sequence, Tuple, Mapping, Optional
87

98
import jpy
@@ -32,6 +31,18 @@
3231
_JRingTableTools = jpy.get_type("io.deephaven.engine.table.impl.sources.ring.RingTableTools")
3332
_JSupplier = jpy.get_type('java.util.function.Supplier')
3433
_JFunctionGeneratedTableFactory = jpy.get_type("io.deephaven.engine.table.impl.util.FunctionGeneratedTableFactory")
34+
_JPythonInputTableStatusListenerAdapter = jpy.get_type(
35+
"io.deephaven.integrations.python.PythonInputTableStatusListenerAdapter")
36+
37+
_DEFAULT_INPUT_TABLE_ON_ERROR_CALLBACK = lambda e: print(f"An error occurred during InputTable async operation: {e}")
38+
39+
40+
def _error_callback_wrapper(callback: Callable[[Exception], None]):
41+
@wraps(callback)
42+
def wrapper(e):
43+
callback(RuntimeError(e))
44+
45+
return wrapper
3546

3647

3748
def empty_table(size: int) -> Table:
@@ -243,8 +254,8 @@ def __init__(self, j_table: jpy.JType):
243254
raise DHError("the provided table's InputTable attribute type is not of InputTableUpdater type.")
244255

245256
def add(self, table: Table) -> None:
246-
"""Synchronously writes rows from the provided table to this input table. If this is a keyed input table, added rows with keys
247-
that match existing rows will replace those rows.
257+
"""Synchronously writes rows from the provided table to this input table. If this is a keyed input table,
258+
added rows with keys that match existing rows will replace those rows.
248259
249260
Args:
250261
table (Table): the table that provides the rows to write
@@ -258,8 +269,8 @@ def add(self, table: Table) -> None:
258269
raise DHError(e, "add to InputTable failed.") from e
259270

260271
def delete(self, table: Table) -> None:
261-
"""Synchronously deletes the keys contained in the provided table from this keyed input table. If this method is called on an
262-
append-only input table, an error will be raised.
272+
"""Synchronously deletes the keys contained in the provided table from this keyed input table. If this
273+
method is called on an append-only input table, an error will be raised.
263274
264275
Args:
265276
table (Table): the table with the keys to delete
@@ -272,6 +283,76 @@ def delete(self, table: Table) -> None:
272283
except Exception as e:
273284
raise DHError(e, "delete data in the InputTable failed.") from e
274285

286+
def add_async(self, table: Table, on_success: Callable[[], None] = None,
287+
on_error: Callable[[Exception], None] = None) -> None:
288+
"""Asynchronously writes rows from the provided table to this input table. If this is a keyed input table,
289+
added rows with keys that match existing rows will replace those rows. This method returns immediately without
290+
waiting for the operation to complete. If the operation succeeds, the optional on_success callback if provided
291+
will be called. If the operation fails, the optional on_error callback if provided will be called. If on_error
292+
is not provided, a default callback function will be called that simply prints out the received exception.
293+
294+
Note, multiple calls to this method on the same thread will be queued and processed in order. However, ordering
295+
is not guaranteed across threads.
296+
297+
Args:
298+
table (Table): the table that provides the rows to write
299+
on_success (Callable[[], None]): the success callback function, default is None
300+
on_error (Callable[[Exception], None]): the error callback function, default is None. When None, a default
301+
callback function will be provided that simply prints out the received exception. If the callback
302+
function itself raises an exception, the new exception will be logged in the Deephaven server log and
303+
will not be further processed by the server.
304+
305+
Raises:
306+
DHError
307+
"""
308+
try:
309+
if on_error:
310+
on_error_callback = _error_callback_wrapper(on_error)
311+
else:
312+
on_error_callback = _error_callback_wrapper(_DEFAULT_INPUT_TABLE_ON_ERROR_CALLBACK)
313+
314+
j_input_table_status_listener = _JPythonInputTableStatusListenerAdapter.create(on_success,
315+
on_error_callback)
316+
self.j_input_table.addAsync(table.j_table, j_input_table_status_listener)
317+
except Exception as e:
318+
raise DHError(e, "async add to InputTable failed.") from e
319+
320+
def delete_async(self, table: Table, on_success: Callable[[], None] = None,
321+
on_error: Callable[[Exception], None] = None) -> None:
322+
"""Asynchronously deletes the keys contained in the provided table from this keyed input table. If this
323+
method is
324+
called on an append-only input table, an error will be raised. This method returns immediately without
325+
waiting for
326+
the operation to complete. If the operation succeeds, the optional on_success callback if provided
327+
will be called. If the operation fails, the optional on_error callback if provided will be called. If on_error
328+
is not provided, a default callback function will be called that simply prints out the received exception.
329+
330+
Note, multiple calls to this method on the same thread will be queued and processed in order. However, ordering
331+
is not guaranteed across threads.
332+
333+
Args:
334+
table (Table): the table with the keys to delete
335+
on_success (Callable[[], None]): the success callback function, default is None
336+
on_error (Callable[[Exception], None]): the error callback function, default is None. When None, a default
337+
callback function will be provided that simply prints out the received exception. If the callback
338+
function itself raises an exception, the new exception will be logged in the Deephaven server log and
339+
will not be further processed by the server.
340+
341+
Raises:
342+
DHError
343+
"""
344+
try:
345+
if on_error:
346+
on_error_callback = _error_callback_wrapper(on_error)
347+
else:
348+
on_error_callback = _error_callback_wrapper(_DEFAULT_INPUT_TABLE_ON_ERROR_CALLBACK)
349+
350+
j_input_table_status_listener = _JPythonInputTableStatusListenerAdapter.create(on_success,
351+
on_error_callback)
352+
self.j_input_table.deleteAsync(table.j_table, j_input_table_status_listener)
353+
except Exception as e:
354+
raise DHError(e, "async delete data in the InputTable failed.") from e
355+
275356
@property
276357
def key_names(self) -> List[str]:
277358
"""The names of the key columns of the InputTable."""
@@ -354,11 +435,11 @@ def ring_table(parent: Table, capacity: int, initialize: bool = True) -> Table:
354435

355436

356437
def function_generated_table(table_generator: Callable[..., Table],
357-
source_tables: Union[Table, List[Table]] = None,
358-
refresh_interval_ms: int = None,
359-
exec_ctx: ExecutionContext = None,
360-
args: Tuple = (),
361-
kwargs: Dict = {}) -> Table:
438+
source_tables: Union[Table, List[Table]] = None,
439+
refresh_interval_ms: int = None,
440+
exec_ctx: ExecutionContext = None,
441+
args: Tuple = (),
442+
kwargs: Dict = {}) -> Table:
362443
"""Creates an abstract table that is generated by running the table_generator() function. The function will first be
363444
run to generate the table when this method is called, then subsequently either (a) whenever one of the
364445
'source_tables' ticks or (b) after refresh_interval_ms have elapsed. Either 'refresh_interval_ms' or
@@ -368,13 +449,15 @@ def function_generated_table(table_generator: Callable[..., Table],
368449
function-generated tables can create tables that are produced by arbitrary Python logic (including using Pandas or
369450
numpy). They can also be used to retrieve data from external sources (such as files or websites).
370451
371-
The table definition must not change between invocations of the 'table_generator' function, or an exception will be raised.
452+
The table definition must not change between invocations of the 'table_generator' function, or an exception will
453+
be raised.
372454
373455
Note that the 'table_generator' may access data in the sourceTables but should not perform further table operations
374456
on them without careful handling. Table operations may be memoized, and it is possible that a table operation will
375457
return a table created by a previous invocation of the same operation. Since that result will not have been included
376458
in the 'source_table', it's not automatically treated as a dependency for purposes of determining when it's safe to
377-
invoke 'table_generator', allowing races to exist between accessing the operation result and that result's own update
459+
invoke 'table_generator', allowing races to exist between accessing the operation result and that result's own
460+
update
378461
processing. It's best to include all dependencies directly in 'source_table', or only compute on-demand inputs under
379462
a LivenessScope.
380463
@@ -441,6 +524,6 @@ def table_generator_function():
441524
j_function_generated_table = _JFunctionGeneratedTableFactory.create(
442525
table_generator_j_function,
443526
source_j_tables
444-
)
527+
)
445528

446529
return Table(j_function_generated_table)

py/server/deephaven/table_listener.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from deephaven.jcompat import to_sequence, j_list_to_list
1818
from deephaven.table import Table
1919
from deephaven._table_reader import _table_reader_all_dict, _table_reader_chunk_dict
20+
from deephaven.table_factory import _error_callback_wrapper
2021

2122
_JPythonReplayListenerAdapter = jpy.get_type("io.deephaven.integrations.python.PythonReplayListenerAdapter")
2223
_JTableUpdate = jpy.get_type("io.deephaven.engine.table.TableUpdate")
@@ -238,14 +239,6 @@ def _wrap_listener_obj(t: Table, listener: TableListener):
238239
return listener
239240

240241

241-
def _error_callback_wrapper(callback: Callable[[Exception], None]):
242-
@wraps(callback)
243-
def wrapper(e):
244-
callback(RuntimeError(e))
245-
246-
return wrapper
247-
248-
249242
class TableListenerHandle(JObjectWrapper):
250243
"""A handle to manage a table listener's lifecycle."""
251244
j_object_type = _JPythonReplayListenerAdapter

py/server/tests/test_table_factory.py

+56-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import jpy
99
import numpy as np
10-
10+
from time import sleep
1111
from deephaven import DHError, read_csv, time_table, empty_table, merge, merge_sorted, dtypes, new_table, \
1212
input_table, time, _wrapper
1313
from deephaven.column import byte_col, char_col, short_col, bool_col, int_col, long_col, float_col, double_col, \
@@ -430,10 +430,7 @@ def test_instant_array(self):
430430
from deephaven import dtypes as dht
431431
from deephaven import time as dhtu
432432

433-
col_defs_5 = \
434-
{ \
435-
"InstantArray": dht.instant_array \
436-
}
433+
col_defs_5 = {"InstantArray": dht.instant_array}
437434

438435
dtw5 = DynamicTableWriter(col_defs_5)
439436
t5 = dtw5.table
@@ -478,6 +475,60 @@ def test_j_input_wrapping(self):
478475
self.assertFalse(isinstance(t, InputTable))
479476
self.assertTrue(isinstance(t, Table))
480477

478+
def test_input_table_async(self):
479+
cols = [
480+
bool_col(name="Boolean", data=[True, False]),
481+
byte_col(name="Byte", data=(1, -1)),
482+
char_col(name="Char", data='-1'),
483+
short_col(name="Short", data=[1, -1]),
484+
int_col(name="Int", data=[1, -1]),
485+
long_col(name="Long", data=[1, -1]),
486+
long_col(name="NPLong", data=np.array([1, -1], dtype=np.int8)),
487+
float_col(name="Float", data=[1.01, -1.01]),
488+
double_col(name="Double", data=[1.01, -1.01]),
489+
string_col(name="String", data=["foo", "bar"]),
490+
]
491+
t = new_table(cols=cols)
492+
493+
with self.subTest("async add"):
494+
self.assertEqual(t.size, 2)
495+
success_count = 0
496+
def on_success():
497+
nonlocal success_count
498+
success_count += 1
499+
append_only_input_table = input_table(col_defs=t.definition)
500+
append_only_input_table.add_async(t, on_success=on_success)
501+
append_only_input_table.add_async(t, on_success=on_success)
502+
while success_count < 2:
503+
sleep(0.1)
504+
self.assertEqual(append_only_input_table.size, 4)
505+
506+
keyed_input_table = input_table(col_defs=t.definition, key_cols="String")
507+
keyed_input_table.add_async(t, on_success=on_success)
508+
keyed_input_table.add_async(t, on_success=on_success)
509+
while success_count < 4:
510+
sleep(0.1)
511+
self.assertEqual(keyed_input_table.size, 2)
512+
513+
with self.subTest("async delete"):
514+
keyed_input_table = input_table(init_table=t, key_cols=["String", "Double"])
515+
keyed_input_table.delete_async(t.select(["String", "Double"]), on_success=on_success)
516+
while success_count < 5:
517+
sleep(0.1)
518+
self.assertEqual(keyed_input_table.size, 0)
519+
t1 = t.drop_columns("String")
520+
521+
with self.subTest("schema mismatch"):
522+
error_count = 0
523+
def on_error(e: Exception):
524+
nonlocal error_count
525+
error_count += 1
526+
527+
append_only_input_table = input_table(col_defs=t1.definition)
528+
with self.assertRaises(DHError) as cm:
529+
append_only_input_table.add_async(t, on_success=on_success, on_error=on_error)
530+
self.assertEqual(error_count, 0)
531+
481532

482533
if __name__ == '__main__':
483534
unittest.main()

0 commit comments

Comments
 (0)