Skip to content

Commit 2b8977a

Browse files
authored
Merge pull request #3006 from finos/fix-to-arrow-thread
Fix `to_arrow` thread safety
2 parents 6907ef9 + 202e653 commit 2b8977a

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

cpp/perspective/src/cpp/view.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,9 @@ View<CTX_T>::to_arrow(
607607
bool emit_group_by,
608608
bool compress
609609
) const {
610+
PSP_GIL_UNLOCK();
611+
PSP_READ_LOCK(*get_lock());
612+
610613
std::shared_ptr<t_data_slice<CTX_T>> data_slice =
611614
get_data(start_row, end_row, start_col, end_col);
612615
return data_slice_to_arrow(data_slice, emit_group_by, compress);
@@ -620,6 +623,8 @@ View<t_ctx2>::to_csv(
620623
std::int32_t start_col,
621624
std::int32_t end_col
622625
) const {
626+
PSP_GIL_UNLOCK();
627+
PSP_READ_LOCK(*get_lock());
623628

624629
// See generic instance.
625630
if (is_column_only() && m_ctx->unity_get_column_count() == 0) {
@@ -639,6 +644,8 @@ View<t_ctx1>::to_csv(
639644
std::int32_t start_col,
640645
std::int32_t end_col
641646
) const {
647+
PSP_GIL_UNLOCK();
648+
PSP_READ_LOCK(*get_lock());
642649
std::shared_ptr<t_data_slice<t_ctx1>> data_slice =
643650
get_data(start_row, end_row, start_col, end_col);
644651
return data_slice_to_csv(data_slice);
@@ -652,9 +659,11 @@ View<CTX_T>::to_csv(
652659
std::int32_t start_col,
653660
std::int32_t end_col
654661
) const {
662+
PSP_GIL_UNLOCK();
663+
PSP_READ_LOCK(*get_lock());
655664

656-
// Arrow has a big whih miscalculates CSV header size as 1 when there are no
657-
// columns (and hence no rows) in the dataset, so intercept these calls/
665+
// Arrow has a bug which miscalculates CSV header size as 1 when there are
666+
// no columns (and hence no rows) in the dataset, so intercept these calls/
658667
if (m_ctx->unity_get_column_count() == 0) {
659668
return std::make_shared<std::string>("");
660669
}

rust/perspective-python/perspective/tests/multi_threaded/test_multi_threaded.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,57 @@ def update(x):
8484
loop.call_soon_threadsafe(loop.stop)
8585
thread.join()
8686
loop.close()
87+
88+
def test_concurrent_updates_with_limit_tables_are_threadsafe(self):
89+
# This is a tricky tune - at time of writing, 1000 is has a >50%
90+
# chance of triggering this o my dev machine
91+
TEST_ITERATIONS = 1000
92+
global running
93+
perspective_server = Server()
94+
client = perspective_server.new_local_client()
95+
table = client.table(
96+
{"col{}".format(i): "integer" for i in range(100)}, limit=100
97+
)
98+
99+
running = True
100+
101+
# Create an updating thread that overlaps the index alot
102+
def feed(table):
103+
row = {"col{}".format(i): random.randint(0, 100) for i in range(100)}
104+
while running:
105+
table.update([row for _ in range(100)])
106+
107+
thread = threading.Thread(target=feed, args=(table,))
108+
thread.start()
109+
110+
results = []
111+
112+
# Create a thread that serialized the table alot, checking for nulls
113+
def feed2(table):
114+
global running
115+
view = table.view()
116+
while len(results) < TEST_ITERATIONS:
117+
arr = view.to_arrow()
118+
table2 = client.table(arr)
119+
view2 = table2.view()
120+
json = view2.to_json(end_row=1)
121+
view2.delete()
122+
table2.delete()
123+
results.append(json)
124+
125+
view.delete()
126+
running = False
127+
128+
thread2 = threading.Thread(target=feed2, args=(table,))
129+
thread2.start()
130+
131+
thread.join()
132+
thread2.join()
133+
134+
assert table.size() == 100
135+
for result in results:
136+
for row in result:
137+
for col, val in row.items():
138+
assert val is not None
139+
140+
table.delete()

0 commit comments

Comments
 (0)