You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@arrow.apache.org by "paleolimbot (via GitHub)" <gi...@apache.org> on 2023/08/18 16:14:44 UTC

[GitHub] [arrow-adbc] paleolimbot opened a new pull request, #985: feat(r): Add async infrastructure for specific methods

paleolimbot opened a new pull request, #985:
URL: https://github.com/apache/arrow-adbc/pull/985

   This is a follow-up to https://github.com/apache/arrow-nanoarrow/pull/211 , which was closed because the async functionality for interacting with the array stream is more closely tied to the potential async functionality of other ADBC methods.
   
   This can/should remain in draft form for some time...async is hard and this PR is currently intended to act as a starting place to discuss what this would look like.
   
   Thanks to @krlmlr for pushing for this functionality!
   
   A reprex to get started:
   
   ``` r
   # pak::pak("apache/arrow-adbc/r/adbcdrivermanager#984")
   queue <- adbcdrivermanager:::adbc_callback_queue()
   
   # Add a few callbacks
   stream <- nanoarrow::basic_array_stream(list(1:5))
   adbcdrivermanager:::adbc_array_stream_get_next_async(stream, function(array) {
     message("Done 1!")
     print(as.vector(array))
   }, queue = queue)
   
   stream <- nanoarrow::basic_array_stream(list(6:10))
   adbcdrivermanager:::adbc_array_stream_get_next_async(stream, function(array) {
     message("Done 2!")
     print(as.vector(array))
   }, queue = queue)
   
   stream <- nanoarrow::basic_array_stream(list(11:15))
   adbcdrivermanager:::adbc_array_stream_get_next_async(stream, function(array) {
     message("Done 3!")
     print(as.vector(array))
   }, queue = queue)
   
   Sys.sleep(0.5)
   
   # Run callback
   (adbcdrivermanager:::adbc_callback_queue_run_pending(queue))
   #> Done 1!
   #> [1] 1 2 3 4 5
   #> Done 2!
   #> [1]  6  7  8  9 10
   #> Done 3!
   #> [1] 11 12 13 14 15
   #> [1] 3
   
   # Make sure gc() doesn't crash
   gc()
   #>           used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
   #> Ncells  712327 38.1    1340807 71.7         NA  1340807 71.7
   #> Vcells 1268606  9.7    8388608 64.0      16384  1996965 15.3
   ```
   
   <sup>Created on 2023-08-18 with [reprex v2.0.2](https://reprex.tidyverse.org)</sup>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2106566057

   Thanks, Dewey. I like this idea I read here:
   
   - Keep {adbdrivermanager} free of the strong {later} dependency
   - Instead, support a callback that can bring in {later}/{promises}
       - {adbcdrivermanager} could even provide a default callback that uses {later} and fails if the package is not installed
   - Polling is always available
   
   Why do we need to tightly couple execution and data retrieval? I could imagine the following process:
   
   - Creation of the (parameterized or not) query sends it to the server, returns a "bound" or "unbound" promise
   - Optional: Parameter binding can accept that "unbound" promise and return a new "bound" promise
   - Schema retrieval accepts that "bound" promise and returns a new "ready" promise
   - Retrieval of each chunk accepts a "ready" promise and returns another "ready" promise
   - When complete, no more promises are returned
   
   ```mermaid
   graph
       PQ("Creation of the parameterized query") --> |Unbound| PB("Parameter binding")
       PB("Parameter binding") --> |Bound| SR
       NPQ("Creation of the not parameterized query") --> |Bound| SR
       SR("Schema retrieval") --> |Ready| RC
       RC("Retrieval of each chunk") -.-> |Ready| RC
       RC -.-> |NULL| End(Done)
   ```
   
   ![image](https://github.com/apache/arrow-adbc/assets/1741643/dc67e0a9-741d-4722-a180-e32a42616b7e)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [arrow-adbc] krlmlr commented on a diff in pull request #985: feat(r): Add async infrastructure for specific methods

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on code in PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#discussion_r1311105141


##########
r/adbcdrivermanager/tests/testthat/test-async.R:
##########
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+test_that("aync array_stream$get_next", {
+  stream <- nanoarrow::basic_array_stream(list(1:5))
+  queue <- adbc_array_stream_get_next_async(stream, function(array) {
+    expect_identical(nanoarrow::convert_array(array), 1:5)
+  })
+
+  Sys.sleep(0.5)
+  expect_identical(adbc_callback_queue_run_pending(queue), 1)

Review Comment:
   Perhaps:
   
   ```suggestion
     async_called <- FALSE
     queue <- adbc_array_stream_get_next_async(stream, function(array) {
       async_called <<- TRUE
       expect_identical(nanoarrow::convert_array(array), 1:5)
     })
   
     expect_false(async_called)
     expect_identical(adbc_callback_queue_run_pending(queue), 1)
     expect_true(async_called)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [arrow-adbc] paleolimbot commented on pull request #985: feat(r): Add async infrastructure for specific methods

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-1706960502

   Far from usable, but it works! Probably needs some version of "read everything from `stream` with a callback for each batch...I'm not sure that fits nicely into `promises` since there are an indeterminate number of batches.
   
   ``` r
   library(adbcdrivermanager)
   
   db <- adbc_database_init(adbcsqlite::adbcsqlite())
   con <- adbc_connection_init(db)
   
   flights <- nycflights13::flights
   flights$time_hour <- NULL
   flights |>
     write_adbc(con, "flights")
   
   
   # Sync
   stmt <- adbc_statement_init(con)
   adbc_statement_set_sql_query(stmt, "SELECT * from flights")
   stream <- nanoarrow::nanoarrow_allocate_array_stream()
   adbc_statement_execute_query(stmt, stream)
   #> [1] -1
   stream
   #> <nanoarrow_array_stream struct<year: int64, month: int64, day: int64, dep_time: int64, sched_dep_time: int64, dep_delay: double, arr_time: int64, sched_arr_time: int64, arr_delay: double, carrier: string, flight: int64, tailnum: string, origin: string, dest: string, air_time: double, distance: double, hour: double, minute: double>>
   #>  $ get_schema:function ()  
   #>  $ get_next  :function (schema = x$get_schema(), validate = TRUE)  
   #>  $ release   :function ()
   
   value <- NULL
   
   
   # Async
   (promise <- con |> 
     adbc_statement_init() |> 
     adbc_statement_set_sql_query("SELECT * from flights") |> 
     adbcdrivermanager:::adbc_statement_execute_query_promise() |> 
     promises::then(function(stream) {
       adbcdrivermanager:::adbc_array_stream_get_next_promise(stream)
     }) |> 
     promises::then(function(array) {
       tibble::as_tibble(array)
     }) |> 
     promises::then(function(tbl) {
       value <<- tbl
     }))
   #> <Promise [pending]>
   
   value
   #> NULL
   
   while(!later::loop_empty()) {
     later::run_now()
   }
   
   promise
   #> <Promise [fulfilled: tbl_df]>
   value
   #> # A tibble: 1,024 × 18
   #>     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
   #>    <dbl> <dbl> <dbl>    <dbl>          <dbl>     <dbl>    <dbl>          <dbl>
   #>  1  2013     1     1      517            515         2      830            819
   #>  2  2013     1     1      533            529         4      850            830
   #>  3  2013     1     1      542            540         2      923            850
   #>  4  2013     1     1      544            545        -1     1004           1022
   #>  5  2013     1     1      554            600        -6      812            837
   #>  6  2013     1     1      554            558        -4      740            728
   #>  7  2013     1     1      555            600        -5      913            854
   #>  8  2013     1     1      557            600        -3      709            723
   #>  9  2013     1     1      557            600        -3      838            846
   #> 10  2013     1     1      558            600        -2      753            745
   #> # ℹ 1,014 more rows
   #> # ℹ 10 more variables: arr_delay <dbl>, carrier <chr>, flight <dbl>,
   #> #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
   #> #   hour <dbl>, minute <dbl>
   ```
   
   <sup>Created on 2023-09-05 with [reprex v2.0.2](https://reprex.tidyverse.org)</sup>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2171783715

   The working example I have is only with queries that don't require reading in async (the reading in async is implemented but I haven't played with it much).
   
   ``` r
   library(adbcdrivermanager)
   
   # docker compose up postgres-test
   test_uri <- "postgresql://localhost:5432/postgres?user=postgres&password=password"
   
   # Needs to be big or else it doesn't take long enough to test that cancellation works
   flights <- vctrs::vec_rep(nycflights13::flights, 10)
   flights <- nanoarrow::as_nanoarrow_array_stream(flights)
   
   con <- adbc_database_init(adbcpostgresql::adbcpostgresql(), uri = test_uri) |> 
     adbc_connection_init()
   
   stmt <- adbc_statement_init(con, adbc.ingest.target_table = "flights")
   adbc_statement_bind_stream(stmt, flights)
   task <- adbcdrivermanager:::adbc_statement_execute_query_async(stmt)
   
   # You should be able to request cancel using Control-C here
   adbcdrivermanager:::adbc_async_task_wait(task)
   #> $statement
   #> <adbcpostgresql_statement at 0x11d785370> 
   #> List of 1
   #>  $ connection:<adbcpostgresql_connection at 0x11d7e0c10> 
   #> List of 1
   #>   ..$ database:<adbcpostgresql_database at 0x11d7a47c0> 
   #> List of 1
   #>   .. ..$ driver:<adbcpostgresql_driver_postgresql> List of 2
   #>   .. .. ..$ driver_init_func:Class 'adbc_driver_init_func' <externalptr> 
   #>   .. .. ..$ .child_count    : int 0
   #> 
   #> $stream
   #> NULL
   #> 
   #> $rows_affected
   #> [1] 3367760
   
   # In case you want to try this again
   # con |> execute_adbc("DROP TABLE flights;")
   ```
   
   <sup>Created on 2024-06-16 with [reprex v2.1.0](https://reprex.tidyverse.org)</sup>
   
   Another example I was playing with was using the "sleep" version to ensure that it wasn't too costly to make these async calls:
   
   ``` r
   bench::mark(
     a1 = adbcdrivermanager:::adbc_async_sleep(1) |> adbcdrivermanager:::adbc_async_task_wait(),
     a10 = adbcdrivermanager:::adbc_async_sleep(10) |> adbcdrivermanager:::adbc_async_task_wait(),
     a100 = adbcdrivermanager:::adbc_async_sleep(100) |> adbcdrivermanager:::adbc_async_task_wait(),
     a500 = adbcdrivermanager:::adbc_async_sleep(500) |> adbcdrivermanager:::adbc_async_task_wait(),
     check = FALSE
   )
   #> # A tibble: 4 × 6
   #>   expression      min   median `itr/sec` mem_alloc `gc/sec`
   #>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
   #> 1 a1           1.14ms    1.4ms    704.       338KB        0
   #> 2 a10         10.52ms   12.5ms     81.0       560B        0
   #> 3 a100       100.84ms  103.6ms      9.71      560B        0
   #> 4 a500       497.31ms  497.3ms      2.01      560B        0
   ```
   
   <sup>Created on 2024-06-16 with [reprex v2.1.0](https://reprex.tidyverse.org)</sup>
   
   You should be able to try these with promises by turning the `task` into a promise using `as.promise()`. Feel free to post your adventures here so that I have better examples to test with!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2105619870

   Thanks, Dewey, nice!
   
   Regarding chaining: Can we chain promises or futures in C++?
   
   Regarding polling: How does mirai do it? Have you seen the C++ interfaces in later and promises? We might need to add a hard dependency, though, or move the async logic to a package that doesn't mind that dependency.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on code in PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#discussion_r1587679146


##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.
+
+extern "C" SEXP RAdbcNewCallbackQueue(void) { return CallbackQueue::MakeXptr(); }
+
+extern "C" SEXP RAdbcCallbackQueueRunPending(SEXP callback_queue_xptr) {
+  // TODO: Check callback_queue_xptr class
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  return Rf_ScalarReal(queue->RunPending());
+}
+
+extern "C" SEXP RAdbcStatementExecuteQueryAsync(SEXP callback_queue_xptr,
+                                                SEXP statement_xptr, SEXP out_stream_xptr,
+                                                SEXP error_xptr, SEXP callback_env) {
+  // TODO: check array_stream/array/callback queue classes using utils in radbc.h
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  auto out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+  auto error = adbc_from_xptr<AdbcError>(error_xptr);
+
+  // Task handle to ensure the thread pointer is cleaned up
+  SEXP task_xptr = PROTECT(Task::MakeXptr(callback_queue_xptr));
+  SEXP task_symbol = PROTECT(Rf_install("task"));
+  Rf_setVar(task_symbol, task_xptr, callback_env);
+  UNPROTECT(1);
+
+  auto task = reinterpret_cast<Task*>(R_ExternalPtrAddr(task_xptr));
+  CallbackQueue::RCallback callback =
+      queue->InitCallback(callback_env, out_stream_xptr, error_xptr);
+  task->worker = new std::thread([statement, out_stream, error, callback, queue] {

Review Comment:
   I am pretty sure this will launch a new thread every time. I will have to check C++17 since I think we require that now.



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.
+
+extern "C" SEXP RAdbcNewCallbackQueue(void) { return CallbackQueue::MakeXptr(); }
+
+extern "C" SEXP RAdbcCallbackQueueRunPending(SEXP callback_queue_xptr) {
+  // TODO: Check callback_queue_xptr class
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  return Rf_ScalarReal(queue->RunPending());
+}
+
+extern "C" SEXP RAdbcStatementExecuteQueryAsync(SEXP callback_queue_xptr,
+                                                SEXP statement_xptr, SEXP out_stream_xptr,
+                                                SEXP error_xptr, SEXP callback_env) {
+  // TODO: check array_stream/array/callback queue classes using utils in radbc.h
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  auto out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+  auto error = adbc_from_xptr<AdbcError>(error_xptr);
+
+  // Task handle to ensure the thread pointer is cleaned up
+  SEXP task_xptr = PROTECT(Task::MakeXptr(callback_queue_xptr));
+  SEXP task_symbol = PROTECT(Rf_install("task"));
+  Rf_setVar(task_symbol, task_xptr, callback_env);
+  UNPROTECT(1);
+
+  auto task = reinterpret_cast<Task*>(R_ExternalPtrAddr(task_xptr));
+  CallbackQueue::RCallback callback =
+      queue->InitCallback(callback_env, out_stream_xptr, error_xptr);
+  task->worker = new std::thread([statement, out_stream, error, callback, queue] {
+    CallbackQueue::RCallback callback_out = callback;
+    int64_t rows_affected = -1;
+    callback_out.return_code =
+        AdbcStatementExecuteQuery(statement, out_stream, &rows_affected, error);
+    queue->AddCallback(callback_out);
+  });
+
+  UNPROTECT(1);
+  return R_NilValue;

Review Comment:
   I will have to revisit what's happening here but I think that the `callback_env` is what lets it be converted to a promise.



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));

Review Comment:
   I will probably leave the PROTECTs in to keep the pattern of "protect all SEXPs" while in use (although I can see how the value of a variable in some other environment and the symbol to which it was assigned would be already protected).



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.

Review Comment:
   I don't personally see the value proposition for a low-level project like this one that wraps a C API (where we already have a non-exception-based error handling scheme and zero C++ idioms that integrate). Debugging a problem that is many layers deep in a C++ template is also the worst (and opening an issue or PR has not historically gotten a timely response).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2171690521

   Thanks for the effort! I see there are tests with sleeping, does this code already work with database connections? Can you share a working setup? 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2108574928

   Nice! I have a concrete idea in my head about how the first things you described might go but I'm stuck as to how to compose fetching an indeterminate number of results by composing promises (without potentially costly overhead of waiting for a promise to resolve before launching another thread and waiting for *that*). Apologies if there's a simple solution there!
   
   ```r
   some_con |>
     adbc_async_prepare() %...>%
     adbc_async_execute_query() %...>%
     # What happens here?
     (function(array_stream) { what_next() })
   ```
   
   > Why do we need to tightly couple execution and data retrieval?
   
   I have a more concrete idea in my head about how one might fetch an entire result (as a vector of C-level arrays) and then call a callback when that has completed. For example:
   
   ```r
   some_con |>
     adbc_async_prepare() %...>%
     adbc_async_execute_and_read_all() %...>%
     # This would be a basic_array_stream whose arrays have already been fetched
     nanoarrow::convert_array_stream()
   ```
   
   Ideally we'd be building up the data frame result on the R thread while waiting for the next batch (but the infrastructure for that isn't merged into nanoarrow yet).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2106071624

   I'll play with this a bit more and try to get this down to a minimum viable feature that one or both of us can build on.
   
   > Regarding polling: How does mirai do it? 
   
   It looks like this happens mostly in nanonext through a runtime dependency on later's C export:
   
   https://github.com/shikokuchuo/nanonext/blob/b139a84512140ac5643f8f3ecb76a6ce8221c531/src/aio.c#L1292
   
   ...which is perhaps a lower-level version of just vendoring the later function definition: https://github.com/r-lib/later/blob/968542a0b42fa191e6a5a57f180a6af345083bf5/inst/include/later.h#L50
   
   I can play with this a little but off the top of my head, I think here one could pass an optional R callback when the async method is launched, which would invoke the runtime "later" dependency (this would replace the ugly later-scheduling itself thing I have here). If there's no callback, one can still poll the `adbc_async_task`.
   
   > Regarding chaining: Can we chain promises or futures in C++?
   
   As long as we know ahead of time what has to happen and it can all happen on another thread, I think we can just define an async launcher that does it all and wait for it to finish. In this case, it would be `AdbcStatementExecuteQuery` + `ArrowArrayStream::get_schema()` + a bunch of `ArrowArrayStream::get_next()`. If we really need the flexibility to compose things, R-level promise things should work.
   
   > move the async logic to a package that doesn't mind that dependency.
   
   With the runtime loading of the later C callable I think we can do this here, although ADBC is quite nice for this because we could in theory document how to get/call the methods (all of this is ABI stable!).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on code in PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#discussion_r1587008993


##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.
+
+extern "C" SEXP RAdbcNewCallbackQueue(void) { return CallbackQueue::MakeXptr(); }
+
+extern "C" SEXP RAdbcCallbackQueueRunPending(SEXP callback_queue_xptr) {
+  // TODO: Check callback_queue_xptr class
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  return Rf_ScalarReal(queue->RunPending());
+}
+
+extern "C" SEXP RAdbcStatementExecuteQueryAsync(SEXP callback_queue_xptr,
+                                                SEXP statement_xptr, SEXP out_stream_xptr,
+                                                SEXP error_xptr, SEXP callback_env) {
+  // TODO: check array_stream/array/callback queue classes using utils in radbc.h
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  auto out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+  auto error = adbc_from_xptr<AdbcError>(error_xptr);
+
+  // Task handle to ensure the thread pointer is cleaned up
+  SEXP task_xptr = PROTECT(Task::MakeXptr(callback_queue_xptr));
+  SEXP task_symbol = PROTECT(Rf_install("task"));
+  Rf_setVar(task_symbol, task_xptr, callback_env);
+  UNPROTECT(1);
+
+  auto task = reinterpret_cast<Task*>(R_ExternalPtrAddr(task_xptr));
+  CallbackQueue::RCallback callback =
+      queue->InitCallback(callback_env, out_stream_xptr, error_xptr);
+  task->worker = new std::thread([statement, out_stream, error, callback, queue] {

Review Comment:
   Does `std` have a notion of a thread pool, or will perhaps system threads be reused transparently?



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.

Review Comment:
   Would you consider vendoring cpp11?



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));
+    SEXP task_xptr = PROTECT(Rf_findVarInFrame(env_sexp, task_sym));
+    R_SetExternalPtrProtected(task_xptr, R_NilValue);
+    UNPROTECT(2);
+
+    // Set up the call to run the callback
+    SEXP callback_sym = PROTECT(Rf_install("callback"));
+    SEXP return_code_sexp = PROTECT(Rf_ScalarInteger(callback.return_code));
+    SEXP callback_call =
+        PROTECT(Rf_lang4(callback_sym, return_code_sexp, error_xptr, return_value_xptr));
+
+    // Run the callback
+    Rf_eval(callback_call, env_sexp);
+
+    UNPROTECT(6);
+  }
+
+  static void FinalizeXptr(SEXP xptr) {
+    CallbackQueue* queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(xptr));
+    delete queue;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new CallbackQueue(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+
+ private:
+  std::deque<RCallback> pending_callbacks_;
+  std::mutex callbacks_lock_;
+};
+
+// TODO: unlike other parts of this package, these functions make use of
+// C++ standard library functions that might throw exceptions in functions
+// that are directly called from R. These will segfault if an exception is
+// thrown. cpp11 handles this using BEGIN_CPP11 an END_CPP11...we would need
+// similar for safety here.
+
+extern "C" SEXP RAdbcNewCallbackQueue(void) { return CallbackQueue::MakeXptr(); }
+
+extern "C" SEXP RAdbcCallbackQueueRunPending(SEXP callback_queue_xptr) {
+  // TODO: Check callback_queue_xptr class
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  return Rf_ScalarReal(queue->RunPending());
+}
+
+extern "C" SEXP RAdbcStatementExecuteQueryAsync(SEXP callback_queue_xptr,
+                                                SEXP statement_xptr, SEXP out_stream_xptr,
+                                                SEXP error_xptr, SEXP callback_env) {
+  // TODO: check array_stream/array/callback queue classes using utils in radbc.h
+  auto queue = reinterpret_cast<CallbackQueue*>(R_ExternalPtrAddr(callback_queue_xptr));
+  auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  auto out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+  auto error = adbc_from_xptr<AdbcError>(error_xptr);
+
+  // Task handle to ensure the thread pointer is cleaned up
+  SEXP task_xptr = PROTECT(Task::MakeXptr(callback_queue_xptr));
+  SEXP task_symbol = PROTECT(Rf_install("task"));
+  Rf_setVar(task_symbol, task_xptr, callback_env);
+  UNPROTECT(1);
+
+  auto task = reinterpret_cast<Task*>(R_ExternalPtrAddr(task_xptr));
+  CallbackQueue::RCallback callback =
+      queue->InitCallback(callback_env, out_stream_xptr, error_xptr);
+  task->worker = new std::thread([statement, out_stream, error, callback, queue] {
+    CallbackQueue::RCallback callback_out = callback;
+    int64_t rows_affected = -1;
+    callback_out.return_code =
+        AdbcStatementExecuteQuery(statement, out_stream, &rows_affected, error);
+    queue->AddCallback(callback_out);
+  });
+
+  UNPROTECT(1);
+  return R_NilValue;

Review Comment:
   Can we return `task_xptr` here, and in the R layer create a promise bound to this specific task?



##########
r/adbcdrivermanager/src/async.cc:
##########
@@ -0,0 +1,262 @@
+
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <deque>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <adbc.h>
+
+#include "radbc.h"
+
+// A thin wrapper around a std::thread() that ensures that the thread
+// does not leak. This could also maybe just be an external pointer to
+// a std::thread*. The external pointer to the Task holds a strong
+// reference to the external pointer to the CallbackQueue that is
+// released just before the callback is run.
+class Task {
+ public:
+  Task() : worker(nullptr) {}
+
+  std::thread* worker;
+
+  static void FinalizeXptr(SEXP xptr) {
+    Task* task = reinterpret_cast<Task*>(R_ExternalPtrAddr(xptr));
+    if (task->worker != nullptr) {
+      // TODO: check task->worker->joinable()?
+      task->worker->join();
+      delete task->worker;
+    }
+
+    delete task;
+  }
+
+  static SEXP MakeXptr(SEXP shelter = R_NilValue) {
+    SEXP xptr = PROTECT(R_MakeExternalPtr(new Task(), R_NilValue, shelter));
+    R_RegisterCFinalizer(xptr, &FinalizeXptr);
+    UNPROTECT(1);
+    return xptr;
+  }
+};
+
+// A thread-safe queue of callbacks to execute.
+class CallbackQueue {
+ public:
+  // Because the RCallback is used in C++ frames where a longjmp may occur,
+  // the members of this struct must be trivially destructible:
+  // Any destruction that needs to occur must occur via an SEXP
+  // finalizer set on the return_value_xptr. This struct is intentionally
+  // copyable and is passed by value.
+  struct RCallback {
+    // An environment containing a function named "callback". This callback
+    // is executed as callback(return_code, error_xptr, return_value_xptr). The
+    // environment may contain other items that need to stay valid for the lifetime of the
+    // task (e.g., inputs).
+    SEXP env_sexp;
+
+    // An external pointer to an AdbcError*
+    SEXP error_xptr;
+
+    // An external pointer to return_value_ptr (with proper finalizer set as applicable)
+    SEXP return_value_xptr;
+
+    // A return code (e.g., AdbcStatusCode). A return code of 0 indicates success.
+    int return_code;
+
+    // The external pointer address of error_xptr
+    AdbcError* return_error;
+
+    // The external pointer address of return_value_xptr
+    void* return_value_ptr;
+  };
+
+  // Initialize a callback and preserve its SEXP members. This must
+  // be called from the main R thread.
+  RCallback InitCallback(SEXP callback_env, SEXP return_value_xptr = R_NilValue,
+                         SEXP error_xptr = R_NilValue) {
+    RCallback out{callback_env, error_xptr, return_value_xptr,
+                  NA_INTEGER,   nullptr,    nullptr};
+    if (error_xptr != R_NilValue) {
+      out.return_error = reinterpret_cast<AdbcError*>(R_ExternalPtrAddr(error_xptr));
+    }
+
+    if (return_value_xptr != R_NilValue) {
+      out.return_value_ptr = R_ExternalPtrAddr(return_value_xptr);
+    }
+
+    R_PreserveObject(out.env_sexp);
+    R_PreserveObject(out.error_xptr);
+    R_PreserveObject(out.return_value_xptr);
+
+    return out;
+  }
+
+  // Add a callback to the queue with thread safety. This can
+  // (should) be called from another thread.
+  void AddCallback(RCallback callback) {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    pending_callbacks_.push_back(callback);
+  }
+
+  // Try to run all callbacks. Callbacks should be written such that
+  // they do not error; however, this function is written such that
+  // an erroring callback will simply result in a (potentially)
+  // incompletely executed callback queue.
+  int64_t RunPending() {
+    int64_t n_run = 0;
+    while (!pending_callbacks_.empty()) {
+      // RunCallback() may may longjmp
+      RCallback callback = GetNextCallback();
+      RunCallback(callback);
+      n_run++;
+    }
+
+    return n_run;
+  }
+
+  // Pop a callback from the end of the queue with thread safety.
+  RCallback GetNextCallback() {
+    std::lock_guard<std::mutex> lock(callbacks_lock_);
+    RCallback callback = pending_callbacks_.front();
+    pending_callbacks_.pop_front();
+    return callback;
+  }
+
+  // Transfer SEXP ownership of callback members to the stack and run
+  // the callback. This must be called from the main R thread.
+  void RunCallback(RCallback callback) {
+    // Transfer responsibility of releasing SEXPs to the stack such that
+    // they will be garbage collected if any of the R calls below error
+    SEXP env_sexp = PROTECT(callback.env_sexp);
+    SEXP error_xptr = PROTECT(callback.error_xptr);
+    SEXP return_value_xptr = PROTECT(callback.return_value_xptr);
+    R_ReleaseObject(env_sexp);
+    R_ReleaseObject(error_xptr);
+    R_ReleaseObject(return_value_xptr);
+
+    // Release the dependence of the task on this callback queue
+    SEXP task_sym = PROTECT(Rf_install("task"));

Review Comment:
   These are typically fine without protection.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2120921024

   Thanks, Dewey. I was really thinking about:
   
   ```r
   some_con |>
     adbc_async_prepare() %...>%
     adbc_async_execute_query() %...>%
     adbc_async_fetch_chunk() %...>%
     adbc_async_fetch_chunk() %...>%
     adbc_async_fetch_chunk() %...>%
     ...
   ```
   
   With:
   
   ```r
   adbc_async_fetch_chunk <- function() {
     if (fetch_thread_running()) {
       ask_fetch_thread_get_more_data()
     } else {
       start_new_fetch_thread()
     }
   }
   ```
   
   Avoiding race conditions is left as an exercise to the reader 🤣 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(r): Add async infrastructure for specific methods [arrow-adbc]

Posted by "paleolimbot (via GitHub)" <gi...@apache.org>.
paleolimbot commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-2097368791

   @krlmlr I updated this a bit with a new approach that floated in as I was working on some other things. The callback-based approach came from me working with Arrow C++, where we have a lot of helpers for this kind of thing. I think the new approach (based on `std::future`) is simpler and generalizes better; however I'm happy to revert to the previous approach if you think it will integrate better. I wish that `later` provided a cleaner way to poll something than to reschedule a task every xxx seconds...obviously polling is suboptimal but I think here we're stuck doing it one way or another.
   
   It would be nice to figure out how `read_adbc_async()` or `write_adbc_async()` or `execute_adbc_async()` might look...`read_adbc_async()` would be the most difficult because there are at 2-3 async calls that might have to get chained together (or we could wrap them all in a single async task that collects the entire array stream). I think there also needs to be some way to "lock" a connection to prevent other things from happening while stuff is happening on another thread (or maybe that's built in to the drivers?).
   
   The code I'm using to play with this is:
   
   ```r
   adbc_async_sleep(10000) |> 
     adbc_async_task_wait()
   ```
   
   ...to ensure that `adbc_async_task_wait()` can be interrupted and
   
   ```r
   library(adbcdrivermanager)
   
   flights <- nycflights13::flights
   
   con <- adbc_database_init(adbcpostgresql::adbcpostgresql(), 
                             uri = Sys.getenv("ADBC_POSTGRESQL_TEST_URI")) |> 
     adbc_connection_init()
   
   stmt <- adbc_statement_init(con, adbc.ingest.target_table = "flights")
   adbc_statement_bind_stream(stmt, flights)
   task <- adbc_statement_execute_query_async(stmt)
   # You should be able to request cancel using Control-C here
   adbc_async_task_wait(task)
   ```
   
   ...for a more real-world scenario.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [arrow-adbc] krlmlr commented on pull request #985: feat(r): Add async infrastructure for specific methods

Posted by "krlmlr (via GitHub)" <gi...@apache.org>.
krlmlr commented on PR #985:
URL: https://github.com/apache/arrow-adbc/pull/985#issuecomment-1708665995

   Wow, amazing! Let me digest this, but your use of promises looks like the thing I was looking for.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org