galaaz 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/LICENSE +0 -0
- data/README.md +1360 -636
- data/Rakefile +61 -41
- data/bin/galaaz-bootstrap +137 -0
- data/bin/galaaz-jruby +14 -0
- data/bin/galaaz_jruby_env.inc.sh +6 -0
- data/bin/gbookdown +64 -0
- data/bin/gknit +84 -13
- data/bin/gknit-draft.rb +0 -0
- data/bin/gstudio +5 -3
- data/bin/gstudio_irb.rb +0 -0
- data/bin/gstudio_pry.rb +0 -0
- data/bin/install-tinytex +6 -0
- data/bin/run_all_rspec +43 -0
- data/bin/run_example +14 -0
- data/bin/run_old_rspec +19 -0
- data/bin/run_rspec +23 -0
- data/bin/run_rspec_subset +38 -0
- data/bin/run_slow_rspec +19 -0
- data/blogs/R-on-Rails-Planning-Document.md +940 -0
- data/blogs/README.md +100 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot.Rmd +38 -66
- data/blogs/galaaz_ggplot/galaaz_ggplot.log +754 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot.md +115 -155
- data/blogs/galaaz_ggplot/galaaz_ggplot.tex +607 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-html/midwest_rb.png +0 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-html/scatter_plot_rb.png +0 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-markdown_github/midwest_rb.png +0 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-markdown_github/scatter_plot_rb.png +0 -0
- data/blogs/galaaz_ggplot/midwest.Rmd +3 -3
- data/blogs/galaaz_ggplot/midwest_external_png +0 -0
- data/blogs/gknit/gknit.Rmd +47 -52
- data/blogs/gknit/gknit.md +1430 -0
- data/blogs/gknit/gknit_files/figure-html/bubble-1.png +0 -0
- data/blogs/gknit/gknit_files/figure-html/diverging_bar.png +0 -0
- data/blogs/gknit/lst.rds +0 -0
- data/blogs/gknit/model.rb +1 -1
- data/blogs/gknit/stats.bib +0 -0
- data/blogs/manual/include_model_local_repro.Rmd +14 -0
- data/blogs/manual/include_model_local_repro.md +75 -0
- data/blogs/manual/lst.rds +0 -0
- data/blogs/manual/manual.Rmd +852 -239
- data/blogs/manual/manual.log +1786 -0
- data/blogs/manual/manual.md +1360 -636
- data/blogs/manual/manual.tex +1883 -1161
- data/blogs/manual/manual_files/figure-html/bubble-1.png +0 -0
- data/blogs/manual/manual_files/figure-html/diverging_bar.png +0 -0
- data/blogs/manual/manual_files/figure-latex/bubble-1.png +0 -0
- data/blogs/manual/model.rb +1 -1
- data/blogs/nse_dplyr/nse_dplyr.Rmd +84 -111
- data/blogs/nse_dplyr/nse_dplyr.log +928 -0
- data/blogs/nse_dplyr/nse_dplyr.md +198 -229
- data/blogs/oh_my/not_so.rb +0 -0
- data/blogs/oh_my/oh_my.Rmd +1234 -25
- data/blogs/oh_my/oh_my.log +804 -0
- data/blogs/oh_my/oh_my.md +1663 -86
- data/blogs/oh_my/oh_my.tex +821 -0
- data/blogs/oh_my/old.Rmd +15 -14
- data/blogs/ruby_plot/ruby_plot.Rmd +58 -82
- data/blogs/ruby_plot/ruby_plot.log +885 -0
- data/blogs/ruby_plot/ruby_plot.md +71 -102
- data/blogs/ruby_plot/ruby_plot.tex +940 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/dose_len.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facet_by_delivery.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facet_by_dose.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_by_delivery_color.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_by_delivery_color2.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_with_decorations.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_with_points.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/final_box_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/final_violin_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/violin_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/dose_len.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facet_by_delivery.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facet_by_dose.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_by_delivery_color.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_by_delivery_color2.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_with_decorations.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_with_points.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/final_box_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/final_violin_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/violin_with_jitter.png +0 -0
- data/blogs/test/test.Rmd +14 -0
- data/examples/50Plots_MasterList/Images/midwest-scatterplot.PNG +0 -0
- data/examples/50Plots_MasterList/ScatterPlot.rb +0 -0
- data/examples/50Plots_MasterList/scatter_plot.rb +0 -0
- data/examples/Bibliography/master.bib +0 -0
- data/examples/Bibliography/stats.bib +0 -0
- data/examples/R/calc.R +0 -0
- data/examples/R/java_interop.R +0 -0
- data/examples/bioconductor_deseq2_airway/Documentation/DESeq2-airway-walkthrough.md +56 -0
- data/examples/bioconductor_deseq2_airway/bench_galaaz_three_same_process.rb +53 -0
- data/examples/bioconductor_deseq2_airway/bench_r_three_same_process.R +34 -0
- data/examples/bioconductor_deseq2_airway/deseq2_airway_galaaz.rb +33 -0
- data/examples/bioconductor_deseq2_airway/deseq2_airway_galaaz_optimized.rb +34 -0
- data/examples/bioconductor_deseq2_airway/deseq2_airway_minimal.R +30 -0
- data/examples/bioconductor_deseq2_airway/deseq2_airway_pipeline_for_bench.R +36 -0
- data/examples/islr/all.rb +13 -0
- data/examples/islr/ch2.spec.rb +37 -7
- data/examples/islr/ch3.spec.rb +11 -2
- data/examples/islr/ch3_boston.rb +27 -0
- data/examples/islr/ch3_multiple_regression.rb +0 -0
- data/examples/islr/ch6.spec.rb +24 -1
- data/examples/islr/x_y_rnorm.jpg +0 -0
- data/examples/latex_templates/Test-acm_article/acm_proc_article-sp.cls +0 -0
- data/examples/latex_templates/Test-acm_article/sigproc.bib +0 -0
- data/examples/latex_templates/Test-acs_article/acs-Test-acs_article.bib +0 -0
- data/examples/latex_templates/Test-acs_article/acs-my_output.bib +0 -0
- data/examples/latex_templates/Test-aea_article/BibFile.bib +0 -0
- data/examples/latex_templates/Test-aea_article/Test-aea_article.Rmd +0 -0
- data/examples/latex_templates/Test-aea_article/references.bib +0 -0
- data/examples/latex_templates/Test-amq_article/Test-amq_article.Rmd +0 -0
- data/examples/latex_templates/Test-amq_article/Test-amq_article.pdfsync +0 -0
- data/examples/latex_templates/Test-ieee_article/IEEEtran.bst +0 -0
- data/examples/latex_templates/Test-ieee_article/mybibfile.bib +0 -0
- data/examples/latex_templates/Test-rjournal_article/RJournal.sty +0 -0
- data/examples/latex_templates/Test-rjournal_article/RJreferences.bib +0 -0
- data/examples/latex_templates/Test-rjournal_article/Test-rjournal_article.Rmd +0 -0
- data/examples/misc/baseball.csv +0 -0
- data/examples/misc/ggplot.rb +3 -2
- data/examples/misc/moneyball.rb +0 -0
- data/examples/misc/subsetting.rb +0 -0
- data/examples/multithread_shards_to_r/shards_to_r.rb +67 -0
- data/examples/rmarkdown/svm-rmarkdown-anon-ms-example/svm-rmarkdown-anon-ms-example.Rmd +0 -0
- data/examples/rmarkdown/svm-rmarkdown-article-example/svm-rmarkdown-article-example.Rmd +0 -0
- data/examples/rmarkdown/svm-rmarkdown-beamer-example/svm-rmarkdown-beamer-example.Rmd +0 -0
- data/examples/rmarkdown/svm-rmarkdown-cv/svm-rmarkdown-cv.Rmd +0 -0
- data/examples/rmarkdown/svm-rmarkdown-syllabus-example/attend-grade-relationships.csv +0 -0
- data/examples/rmarkdown/svm-rmarkdown-syllabus-example/svm-rmarkdown-syllabus-example.Rmd +0 -0
- data/examples/rmarkdown/svm-xaringan-example/svm-xaringan-example.Rmd +0 -0
- data/examples/sthda_ggplot/README.md +0 -0
- data/examples/sthda_ggplot/RUN.md +41 -0
- data/examples/sthda_ggplot/all.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/density_gg.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/geom_area.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/geom_density.rb +2 -0
- data/examples/sthda_ggplot/one_variable_continuous/geom_dotplot.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/geom_freqpoly.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/geom_histogram.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/histogram_density.rb +0 -0
- data/examples/sthda_ggplot/one_variable_continuous/stat.rb +0 -0
- data/examples/sthda_ggplot/one_variable_discrete/bar.rb +0 -0
- data/examples/sthda_ggplot/qplots/box_violin_dot.rb +0 -0
- data/examples/sthda_ggplot/qplots/scatter_plots.rb +0 -0
- data/examples/sthda_ggplot/scatter_gg.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_bin2d.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_density2d.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_hex.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_cont/geom_point.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_cont/geom_smooth.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_cont/misc.rb +0 -0
- data/examples/sthda_ggplot/two_variables_cont_function/geom_area.rb +4 -3
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_bar.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_boxplot.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_dotplot.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_jitter.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_line.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_cont/geom_violin.rb +0 -0
- data/examples/sthda_ggplot/two_variables_disc_disc/geom_jitter.rb +0 -0
- data/examples/sthda_ggplot/two_variables_error/geom_crossbar.rb +0 -0
- data/ext/new_bridge/Makefile +46 -0
- data/ext/new_bridge/galaaz_gatekeeper_phase0.cpp +12 -0
- data/ext/new_bridge/galaaz_gatekeeper_phase1.cpp +1639 -0
- data/lib/R_interface/galaaz_device.R +20 -0
- data/lib/R_interface/include_engine.R +109 -0
- data/lib/R_interface/new_bridge_adapter.rb +824 -0
- data/lib/R_interface/r.rb +177 -25
- data/lib/R_interface/r_arrow.rb +113 -0
- data/lib/R_interface/r_libs.R +3 -3
- data/lib/R_interface/r_methods.rb +13 -126
- data/lib/R_interface/r_module_s.rb +0 -0
- data/lib/R_interface/rbinary_operators.rb +20 -2
- data/lib/R_interface/rclosure.rb +5 -1
- data/lib/R_interface/rdata_frame.rb +34 -70
- data/lib/R_interface/rdevice.rb +125 -0
- data/lib/R_interface/rdevices.R +0 -0
- data/lib/R_interface/renvironment.rb +10 -4
- data/lib/R_interface/rexpression.rb +5 -1
- data/lib/R_interface/rindexed_object.rb +41 -13
- data/lib/R_interface/rlanguage.rb +20 -62
- data/lib/R_interface/rlist.rb +115 -25
- data/lib/R_interface/rlogical_operators.rb +0 -0
- data/lib/R_interface/rmatrix.rb +2 -11
- data/lib/R_interface/rmd_indexed_object.rb +5 -1
- data/lib/R_interface/robject.rb +348 -290
- data/lib/R_interface/rpkg.rb +0 -0
- data/lib/R_interface/rsupport.rb +609 -328
- data/lib/R_interface/rsupport_scope.rb +2 -1
- data/lib/R_interface/rsymbol.rb +50 -0
- data/lib/R_interface/ruby_callback.rb +2 -3
- data/lib/R_interface/ruby_extensions.rb +225 -175
- data/lib/R_interface/runary_operators.rb +0 -0
- data/lib/R_interface/rvector.rb +147 -31
- data/lib/galaaz.rb +0 -0
- data/lib/galaaz_jruby.rb +22 -0
- data/lib/gknit/diagnostics.rb +50 -0
- data/lib/gknit/draft.rb +23 -17
- data/lib/gknit/include_engine.rb +15 -7
- data/lib/gknit/knitr_engine.rb +223 -74
- data/lib/gknit/rb_engine.rb +3 -3
- data/lib/gknit/ruby_engine.rb +0 -0
- data/lib/gknit.rb +1 -0
- data/lib/new_bridge/bootstrap/windows_bootstrap.rb +285 -0
- data/lib/new_bridge/envelope.rb +51 -0
- data/lib/new_bridge/eval_result.rb +26 -0
- data/lib/new_bridge/framing.rb +39 -0
- data/lib/new_bridge/instance_pool_client.rb +38 -0
- data/lib/new_bridge/r_instance_manager.rb +404 -0
- data/lib/new_bridge/session_client.rb +530 -0
- data/lib/new_bridge/tcp_framed.rb +44 -0
- data/lib/new_bridge.rb +9 -0
- data/lib/util/exec_ruby.rb +95 -20
- data/lib/util/inline_file.rb +35 -30
- data/new_bridge_specs/benchmark_phase5_5_unboxing_spec.rb +96 -0
- data/new_bridge_specs/eval_r_async_spec.rb +113 -0
- data/new_bridge_specs/integration_phase5_1_concurrent_spec.rb +50 -0
- data/new_bridge_specs/integration_phase5_1_eval_spec.rb +16 -0
- data/new_bridge_specs/integration_phase5_1_r_api_spec.rb +25 -0
- data/new_bridge_specs/integration_phase5_1_smoke_spec.rb +31 -0
- data/new_bridge_specs/integration_phase5_2_dataframe_unboxing_spec.rb +19 -0
- data/new_bridge_specs/integration_phase5_2_handle_eval_unboxing_spec.rb +25 -0
- data/new_bridge_specs/integration_phase5_3_callback_args_spec.rb +28 -0
- data/new_bridge_specs/integration_phase5_3_callback_error_spec.rb +22 -0
- data/new_bridge_specs/integration_phase5_3_callback_timeout_spec.rb +28 -0
- data/new_bridge_specs/integration_phase5_3_callbacks_smoke_spec.rb +22 -0
- data/new_bridge_specs/integration_phase5_3_edge_cases_spec.rb +52 -0
- data/new_bridge_specs/integration_phase5_3_nested_spec.rb +30 -0
- data/new_bridge_specs/integration_phase5_4_concurrent_sessions_spec.rb +53 -0
- data/new_bridge_specs/integration_phase5_4_nested_session_callbacks_spec.rb +49 -0
- data/new_bridge_specs/integration_phase5_4_session_routing_spec.rb +38 -0
- data/new_bridge_specs/integration_phase5_5_stress_concurrency_spec.rb +52 -0
- data/new_bridge_specs/integration_phase5_5_unbox_walk_spec.rb +46 -0
- data/new_bridge_specs/phase0_protocol_spec.rb +96 -0
- data/new_bridge_specs/phase1_req_ret_spec.rb +66 -0
- data/new_bridge_specs/phase2_multi_instance_spec.rb +67 -0
- data/new_bridge_specs/phase3_callbacks_spec.rb +71 -0
- data/new_bridge_specs/phase4_2_hardening_spec.rb +252 -0
- data/new_bridge_specs/phase4_3_r_instance_manager_spec.rb +85 -0
- data/new_bridge_specs/phase4_nested_callbacks_spec.rb +123 -0
- data/r_requires/ggplot.rb +0 -0
- data/r_requires/knitr.rb +0 -0
- data/specs/all.rb +15 -11
- data/specs/arrow_from_ruby_batches_spec.rb +50 -0
- data/specs/arrow_semantics_spec.rb +64 -0
- data/specs/bridge_concurrent_spec.rb +46 -0
- data/specs/bridge_nested_spec.rb +25 -0
- data/specs/dataframe_semantics_spec.rb +122 -0
- data/specs/dataframe_single_index_logical_filter_spec.rb +21 -0
- data/specs/dispatch_probe_cache_spec.rb +38 -0
- data/specs/dispatch_probe_error_class_fallback_spec.rb +20 -0
- data/specs/dispatch_probe_fallback_spec.rb +18 -0
- data/specs/environment_semantics_spec.rb +89 -0
- data/specs/field_access_spec.rb +31 -0
- data/specs/figures/bg.jpeg +0 -0
- data/specs/figures/bg.png +0 -0
- data/specs/figures/bg.svg +168 -57
- data/specs/figures/dose_len.png +0 -0
- data/specs/figures/no_args.jpeg +0 -0
- data/specs/figures/no_args.png +0 -0
- data/specs/figures/no_args.svg +168 -57
- data/specs/figures/width_height.jpeg +0 -0
- data/specs/figures/width_height.png +0 -0
- data/specs/figures/width_height_units1.jpeg +0 -0
- data/specs/figures/width_height_units1.png +0 -0
- data/specs/figures/width_height_units2.jpeg +0 -0
- data/specs/figures/width_height_units2.png +0 -0
- data/specs/formula_semantics_spec.rb +81 -0
- data/specs/galaaz_util_exec_ruby_spec.rb +85 -0
- data/specs/galaaz_util_inline_file_spec.rb +54 -0
- data/specs/gknit_cli_option_permutation_spec.rb +24 -0
- data/specs/gknit_include_engine_spec.rb +72 -0
- data/specs/gknit_install_timeout_report_spec.rb +69 -0
- data/specs/gknit_internal_error_report_spec.rb +57 -0
- data/specs/gknit_vector_map_output_spec.rb +59 -0
- data/specs/globalenv_guardrail_spec.rb +52 -0
- data/specs/language_expression_semantics_spec.rb +145 -0
- data/specs/list_semantics_spec.rb +111 -0
- data/specs/new_bridge_bulk_dataframe_transfer_spec.rb +44 -0
- data/specs/new_bridge_bulk_vector_transfer_spec.rb +73 -0
- data/specs/new_bridge_callback_timeout_spec.rb +69 -0
- data/specs/new_bridge_eval_r_fallback_spec.rb +55 -0
- data/specs/nil_null_spec.rb +42 -0
- data/specs/object_build_phase2_spec.rb +53 -0
- data/specs/phase1_callback_bridge_spec.rb +84 -0
- data/specs/phase2_gknit_generic_rendering_guardrail_spec.rb +46 -0
- data/specs/phase2_gknit_no_raw_code_leakage_spec.rb +43 -0
- data/specs/phase3_gknit_generic_graphics_capture_spec.rb +71 -0
- data/specs/plot_device_semantics_spec.rb +28 -0
- data/specs/plot_snapshot_semantics_spec.rb +58 -0
- data/specs/protocol_result_spec.rb +236 -0
- data/specs/r_batch_fail_fast_spec.rb +47 -0
- data/specs/r_bridge_bootstrap_spec.rb +11 -0
- data/specs/r_devices.spec.rb +1 -1
- data/specs/r_eval.spec.rb +16 -18
- data/specs/r_function.spec.rb +1 -1
- data/specs/r_instance_manager_spec.rb +285 -0
- data/specs/r_list_apply.spec.rb +15 -15
- data/specs/r_matrix.spec.rb +0 -0
- data/specs/r_nse.spec.rb +5 -5
- data/specs/r_object_send_dispatch_spec.rb +13 -0
- data/specs/r_vector_comparator_spec.rb +8 -0
- data/specs/r_vector_creation.spec.rb +0 -0
- data/specs/r_vector_functions.spec.rb +0 -0
- data/specs/r_vector_object.spec.rb +0 -0
- data/specs/r_vector_operators.spec.rb +0 -0
- data/specs/r_vector_structured_scalar_reads_spec.rb +35 -0
- data/specs/r_vector_subsetting.spec.rb +0 -0
- data/specs/range_helper_spec.rb +21 -0
- data/specs/rsupport_scope_spec.rb +28 -0
- data/specs/rsupport_var_name_thread_safety_spec.rb +24 -0
- data/specs/scalar_character_spec.rb +44 -0
- data/specs/scoped_symbol_dsl_refinement_spec.rb +40 -0
- data/specs/session_env_bridge_spec.rb +25 -0
- data/specs/simplecov_bootstrap_spec.rb +10 -0
- data/specs/spec_helper.rb +10 -0
- data/specs/tmp.rb +0 -0
- data/specs/unboxing_recursion_regression_spec.rb +30 -0
- data/specs/unboxing_spec.rb +49 -0
- data/specs/verify_callbacks.rb +42 -0
- data/sty/galaaz.sty +0 -0
- data/version.rb +1 -1
- metadata +194 -64
- data/blogs/galaaz_ggplot/galaaz_ggplot.html +0 -520
- data/blogs/galaaz_ggplot/galaaz_ggplot.pdf +0 -0
- data/blogs/galaaz_ggplot/midwest.html +0 -188
- data/blogs/gknit/gknit.html +0 -2266
- data/blogs/gknit/gknit.pdf +0 -0
- data/blogs/manual/manual.html +0 -4638
- data/blogs/manual/manual.pdf +0 -0
- data/blogs/manual/manual_files/figure-latex/diverging_bar.pdf +0 -0
- data/blogs/nse_dplyr/nse_dplyr.html +0 -878
- data/blogs/nse_dplyr/nse_dplyr.pdf +0 -0
- data/blogs/oh_my/oh_my.html +0 -568
- data/blogs/ruby_plot/ruby_plot.html +0 -544
- data/blogs/ruby_plot/ruby_plot.pdf +0 -0
- data/examples/latex_templates/Test-acs_article/Test-acs_article.pdf +0 -0
- data/examples/latex_templates/Test-aea_article/Test-aea_article.pdf +0 -0
- data/examples/latex_templates/Test-amq_article/Test-amq_article.pdf +0 -0
- data/examples/latex_templates/Test-amq_article/pics/Figure2.pdf +0 -0
- data/examples/latex_templates/Test-asa_article/Test-asa_article.pdf +0 -0
- data/examples/latex_templates/Test-ieee_article/Test-ieee_article.pdf +0 -0
- data/examples/latex_templates/Test-rjournal_article/RJwrapper.pdf +0 -0
- data/examples/latex_templates/Test-springer_article/Test-springer_article.pdf +0 -0
- data/examples/rmarkdown/svm-rmarkdown-anon-ms-example/svm-rmarkdown-anon-ms-example.pdf +0 -0
- data/examples/rmarkdown/svm-rmarkdown-article-example/svm-rmarkdown-article-example.pdf +0 -0
- data/examples/rmarkdown/svm-rmarkdown-beamer-example/svm-rmarkdown-beamer-example.pdf +0 -0
- data/examples/rmarkdown/svm-rmarkdown-cv/svm-rmarkdown-cv.pdf +0 -0
- data/examples/rmarkdown/svm-rmarkdown-syllabus-example/svm-rmarkdown-syllabus-example.pdf +0 -0
- data/specs/r_dataframe.spec.rb +0 -379
- data/specs/r_environment.spec.rb +0 -140
- data/specs/r_formula.spec.rb +0 -232
- data/specs/r_language.spec.rb +0 -112
- data/specs/r_list.spec.rb +0 -293
- data/specs/r_plots.spec.rb +0 -72
- data/specs/ruby_expression.spec.rb +0 -316
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
|
|
9
|
+
require_relative 'envelope'
|
|
10
|
+
require_relative 'eval_result'
|
|
11
|
+
require_relative 'framing'
|
|
12
|
+
|
|
13
|
+
module NewBridge
|
|
14
|
+
# Pending async +eval_r_async+ entry: completion runs on a background thread (not the reader).
|
|
15
|
+
class AsyncPendingEntry
|
|
16
|
+
attr_reader :instance_id, :block
|
|
17
|
+
attr_accessor :timer_thread
|
|
18
|
+
|
|
19
|
+
def initialize(instance_id, block)
|
|
20
|
+
@instance_id = instance_id
|
|
21
|
+
@block = block
|
|
22
|
+
@timer_thread = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cancel_timer!
|
|
26
|
+
t = @timer_thread
|
|
27
|
+
return unless t&.alive?
|
|
28
|
+
return if Thread.current.equal?(t)
|
|
29
|
+
|
|
30
|
+
t.kill
|
|
31
|
+
t.join(0.05)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# NewBridge::SessionClient
|
|
38
|
+
#
|
|
39
|
+
# Responsibilities (Ruby side, inside one Ruby process):
|
|
40
|
+
# - Accept one connection from the R gatekeeper (Unix socket or TCP).
|
|
41
|
+
# - Serialize and send framed MsgPack envelopes of type `REQ`.
|
|
42
|
+
# - Wait for matching framed MsgPack envelopes of type `RET`.
|
|
43
|
+
# - Handle R -> Ruby callback requests (`CALL`) by invoking registered Ruby
|
|
44
|
+
# blocks in background threads and replying with `RET`.
|
|
45
|
+
#
|
|
46
|
+
# Protocol basics
|
|
47
|
+
# - Transport: Unix domain socket for local R (loopback), TCP when R connects to a non-local host
|
|
48
|
+
# (e.g. Docker). Override with ENV['GALAAZ_BRIDGE_TRANSPORT'] = unix|tcp. Same MsgPack framing.
|
|
49
|
+
# - Envelopes are MsgPack maps handled by `NewBridge::Envelope`.
|
|
50
|
+
# - Envelope fields used here:
|
|
51
|
+
# - `call_id`: UUID used to correlate one REQ with one RET
|
|
52
|
+
# - `type`: `"REQ"` / `"RET"` / `"CALL"`
|
|
53
|
+
# - `session_id`: logical session; R uses it to isolate per-session env state
|
|
54
|
+
# - `instance_id`: logical instance routing; used for multi-instance pools
|
|
55
|
+
# - `parent_id`: reserved for nested-call correlation (Phase 4+)
|
|
56
|
+
#
|
|
57
|
+
# Threading model
|
|
58
|
+
# - One reader thread (`reader_loop`) continuously reads frames and dispatches:
|
|
59
|
+
# - `RET` -> pushes into the per-call Queue (sync +eval_r+) or schedules async completion (+eval_r_async+)
|
|
60
|
+
# - `CALL` -> spawns a new thread to execute the Ruby callback
|
|
61
|
+
# - Callback threads send `RET` back to R while keeping the reader thread free,
|
|
62
|
+
# which is required to support nested REQ servicing while waiting for a callback.
|
|
63
|
+
#
|
|
64
|
+
# Failure behavior
|
|
65
|
+
# - If the socket closes or a read error happens, pending REQ calls are signaled
|
|
66
|
+
# as closed and subsequent eval calls raise.
|
|
67
|
+
# - Infrastructure failures (timeouts / broken pipes / connection reset) are
|
|
68
|
+
# handled at a higher level by `RInstanceManager` restart+retry logic.
|
|
69
|
+
class SessionClient
|
|
70
|
+
class Error < StandardError; end
|
|
71
|
+
class TimeoutError < Error; end
|
|
72
|
+
class RProcessError < Error; end
|
|
73
|
+
|
|
74
|
+
attr_reader :r_stderr, :r_exit_status
|
|
75
|
+
|
|
76
|
+
# Create a SessionClient for one R runtime instance.
|
|
77
|
+
#
|
|
78
|
+
# @param source_path [String] Host-side path to .cpp or .so for Rcpp `sourceCpp`/`dyn.load`.
|
|
79
|
+
# @param host [String] bind/listen address for Ruby TCP server.
|
|
80
|
+
# @param bridge_host [String,nil] host name/path R uses to connect to this Ruby listener (defaults to +host+).
|
|
81
|
+
# For containerized R, set to the host reachable from the container (e.g. host.docker.internal).
|
|
82
|
+
# @param runtime_source_path [String,nil] source path visible from runtime process/container.
|
|
83
|
+
# @param use_unix [Boolean,nil] force Unix (true) or TCP (false). Default nil: auto — Unix for local
|
|
84
|
+
# loopback targets, TCP for remote/non-loopback +bridge_host+.
|
|
85
|
+
# @param r_cmd [String, Array<String>] executable (or argv prefix ending with executable).
|
|
86
|
+
def initialize(source_path:, host: '127.0.0.1', bridge_host: nil, runtime_source_path: nil, r_cmd: 'R',
|
|
87
|
+
use_unix: nil)
|
|
88
|
+
@source_path = File.expand_path(source_path)
|
|
89
|
+
@use_precompiled = @source_path.end_with?('.so')
|
|
90
|
+
@host = host
|
|
91
|
+
@bridge_host = bridge_host || host
|
|
92
|
+
@runtime_source_path = runtime_source_path || @source_path
|
|
93
|
+
@r_cmd = r_cmd
|
|
94
|
+
@use_unix = if use_unix.nil?
|
|
95
|
+
self.class.default_use_unix?(bridge_target: @bridge_host)
|
|
96
|
+
else
|
|
97
|
+
use_unix
|
|
98
|
+
end
|
|
99
|
+
@unix_path = nil
|
|
100
|
+
@server = nil
|
|
101
|
+
@sock = nil
|
|
102
|
+
@write_mx = Mutex.new
|
|
103
|
+
@pending = {}
|
|
104
|
+
@pending_mx = Mutex.new
|
|
105
|
+
@callbacks = {}
|
|
106
|
+
@callbacks_mx = Mutex.new
|
|
107
|
+
@reader = nil
|
|
108
|
+
@r_stderr = +''
|
|
109
|
+
@r_stderr_mx = Mutex.new
|
|
110
|
+
@r_stderr_limit = 64 * 1024
|
|
111
|
+
@r_exit_status = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether to use Unix domain sockets for this client when +use_unix+ is nil.
|
|
115
|
+
#
|
|
116
|
+
# Policy:
|
|
117
|
+
# - +GALAAZ_BRIDGE_TRANSPORT=tcp+ — always TCP (remote R, or debugging).
|
|
118
|
+
# - +GALAAZ_BRIDGE_TRANSPORT=unix+ — always Unix when supported.
|
|
119
|
+
# - Otherwise: Unix only if +bridge_target+ is a local loopback address/name (127.0.0.1, ::1, localhost)
|
|
120
|
+
# or a Unix socket path (+#+). Any other host (e.g. +host.docker.internal+, LAN IP) uses TCP so R can
|
|
121
|
+
# reach Ruby over the network.
|
|
122
|
+
#
|
|
123
|
+
# @param bridge_target [String] resolved +bridge_host+ passed to R (+galaaz_run_bridge+).
|
|
124
|
+
def self.default_use_unix?(bridge_target:)
|
|
125
|
+
return false unless defined?(UNIXServer)
|
|
126
|
+
|
|
127
|
+
case ENV['GALAAZ_BRIDGE_TRANSPORT'].to_s.downcase
|
|
128
|
+
when 'tcp'
|
|
129
|
+
false
|
|
130
|
+
when 'unix'
|
|
131
|
+
true
|
|
132
|
+
else
|
|
133
|
+
t = bridge_target.to_s.strip
|
|
134
|
+
if t.empty?
|
|
135
|
+
false
|
|
136
|
+
elsif t.start_with?('/')
|
|
137
|
+
true
|
|
138
|
+
else
|
|
139
|
+
u = t.downcase
|
|
140
|
+
%w[127.0.0.1 ::1 localhost].include?(u)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Start listening for a single TCP connection from one R process and launch that process.
|
|
146
|
+
#
|
|
147
|
+
# - Binds to an ephemeral TCP port, or a Unix socket path when +use_unix+ is set.
|
|
148
|
+
# - Starts a thread that runs `r_cmd` and sources the gatekeeper C++ code inside R.
|
|
149
|
+
# - Accepts the runtime connection with `accept_timeout`.
|
|
150
|
+
#
|
|
151
|
+
# On startup failure (e.g. runtime exits before connecting), it surfaces R stderr and exit status.
|
|
152
|
+
def start(accept_timeout: 120)
|
|
153
|
+
use_u = @use_unix && defined?(UNIXServer)
|
|
154
|
+
if @use_unix && !defined?(UNIXServer)
|
|
155
|
+
warn '[NewBridge::SessionClient] Unix bridge transport selected but UNIXServer is unavailable; falling back to TCP'
|
|
156
|
+
use_u = false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if use_u
|
|
160
|
+
@unix_path = File.join(Dir.tmpdir, "galaaz_bridge_#{Process.pid}_#{SecureRandom.hex(6)}.sock")
|
|
161
|
+
File.unlink(@unix_path) if File.exist?(@unix_path)
|
|
162
|
+
@server = UNIXServer.new(@unix_path)
|
|
163
|
+
port = 0
|
|
164
|
+
bridge_for_r = @unix_path.gsub("'", "\\\\'")
|
|
165
|
+
else
|
|
166
|
+
@server = TCPServer.new(@host, 0)
|
|
167
|
+
port = @server.addr[1]
|
|
168
|
+
bridge_for_r = @bridge_host.gsub("'", "\\\\'")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Use sourceCpp with a persistent cache directory so repeated runtime
|
|
172
|
+
# starts can reuse compiled shared objects when source is unchanged.
|
|
173
|
+
cpp_escaped = @runtime_source_path.gsub("'", "\\\\'")
|
|
174
|
+
rcpp_cache_dir = (ENV['GALAAZ_RCPP_CACHE_DIR'] || File.join(Dir.tmpdir, 'galaaz_rcpp_cache')).gsub("'", "\\\\'")
|
|
175
|
+
r_script = <<~R
|
|
176
|
+
bridge_host <- '#{bridge_for_r}'
|
|
177
|
+
port <- #{port}L
|
|
178
|
+
stopifnot(requireNamespace("Rcpp", quietly = TRUE))
|
|
179
|
+
library(Rcpp)
|
|
180
|
+
dir.create("#{rcpp_cache_dir}", recursive = TRUE, showWarnings = FALSE)
|
|
181
|
+
sourceCpp(file = "#{cpp_escaped}", rebuild = FALSE, cacheDir = "#{rcpp_cache_dir}")
|
|
182
|
+
galaaz_run_bridge(bridge_host, as.integer(port))
|
|
183
|
+
R
|
|
184
|
+
|
|
185
|
+
env = if use_u
|
|
186
|
+
{ 'GALAAZ_BRIDGE_HOST' => @unix_path, 'GALAAZ_BRIDGE_PORT' => '0', 'GALAAZ_BRIDGE_UNIX' => '1' }
|
|
187
|
+
else
|
|
188
|
+
{ 'GALAAZ_BRIDGE_HOST' => @bridge_host, 'GALAAZ_BRIDGE_PORT' => port.to_s }
|
|
189
|
+
end
|
|
190
|
+
@r_thr = Thread.new do
|
|
191
|
+
launch = @r_cmd.is_a?(Array) ? @r_cmd.dup : [@r_cmd]
|
|
192
|
+
_stdin, stdout_err, wait_thr = Open3.popen2e(env, *launch, '--slave', '--no-save', '-e', r_script)
|
|
193
|
+
# When the R process exits or during shutdown, the underlying pipe can
|
|
194
|
+
# close while this background thread is still blocked reading.
|
|
195
|
+
# Treat that as a normal shutdown and avoid JRuby "stream closed in
|
|
196
|
+
# another thread" warnings.
|
|
197
|
+
begin
|
|
198
|
+
stdout_err.each_line { |line| append_r_stderr(line) }
|
|
199
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
200
|
+
# normal shutdown / pipe closure
|
|
201
|
+
end
|
|
202
|
+
@r_exit_status = wait_thr.value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
@sock = Timeout.timeout(accept_timeout) { @server.accept }
|
|
206
|
+
@reader = Thread.new { reader_loop }
|
|
207
|
+
self
|
|
208
|
+
rescue Timeout::Error
|
|
209
|
+
# If the runtime process failed early (e.g. docker permission/network/source path),
|
|
210
|
+
# surface stderr/exit details instead of a generic accept timeout.
|
|
211
|
+
process_status = @r_exit_status&.exitstatus
|
|
212
|
+
details = current_r_stderr
|
|
213
|
+
msg = "failed to accept runtime connection within #{accept_timeout}s"
|
|
214
|
+
msg += " (runtime exit=#{process_status})" if process_status
|
|
215
|
+
unless details.empty?
|
|
216
|
+
tail = details.length > 2000 ? details[-2000, 2000] : details
|
|
217
|
+
msg += " stderr_tail=#{tail}"
|
|
218
|
+
end
|
|
219
|
+
raise RProcessError, msg
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Stop the client and attempt to join threads cleanly.
|
|
223
|
+
# Best-effort: socket/server are closed, reader thread is joined, runtime thread is joined.
|
|
224
|
+
def stop
|
|
225
|
+
@sock&.close rescue nil
|
|
226
|
+
@server&.close rescue nil
|
|
227
|
+
if @unix_path && File.socket?(@unix_path)
|
|
228
|
+
File.unlink(@unix_path) rescue nil
|
|
229
|
+
end
|
|
230
|
+
@unix_path = nil
|
|
231
|
+
@reader&.join(3)
|
|
232
|
+
@r_thr&.join(15)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Evaluate one REQ on the connected R runtime and wait for the matching RET.
|
|
236
|
+
#
|
|
237
|
+
# @param code [String] R expression or snippet to be evaluated by the gatekeeper.
|
|
238
|
+
# @param session_id [String] session env isolation key (maps to R's `.galaaz_sessions`).
|
|
239
|
+
# @param instance_id [String] instance routing key (must match RInstanceManager routing).
|
|
240
|
+
# @param parent_id [String,nil] reserved for nested-call correlation (Phase 4+).
|
|
241
|
+
# @param timeout [Numeric] how long to wait for RET before raising TimeoutError.
|
|
242
|
+
#
|
|
243
|
+
# @return [Hash] decoded MsgPack payload map returned by gatekeeper on success.
|
|
244
|
+
def eval_r(code, session_id: 'default', instance_id: 'default', parent_id: nil, timeout: 60)
|
|
245
|
+
call_id = SecureRandom.uuid
|
|
246
|
+
q = Queue.new
|
|
247
|
+
@pending_mx.synchronize { @pending[call_id] = q }
|
|
248
|
+
req = Envelope.encode(
|
|
249
|
+
'call_id' => call_id,
|
|
250
|
+
'type' => 'REQ',
|
|
251
|
+
'session_id' => session_id,
|
|
252
|
+
'instance_id' => instance_id,
|
|
253
|
+
'parent_id' => parent_id,
|
|
254
|
+
'payload' => code
|
|
255
|
+
)
|
|
256
|
+
@write_mx.synchronize { Framing.write_frame(@sock, req) }
|
|
257
|
+
|
|
258
|
+
ret = wait_for_ret(q, call_id, timeout)
|
|
259
|
+
if ret['instance_id'] && ret['instance_id'] != instance_id
|
|
260
|
+
raise RProcessError, "instance_id mismatch: expected=#{instance_id} got=#{ret['instance_id']}"
|
|
261
|
+
end
|
|
262
|
+
status = ret['status']
|
|
263
|
+
payload = ret['payload']
|
|
264
|
+
unless payload.is_a?(Hash)
|
|
265
|
+
raise RProcessError, "invalid RET payload type=#{payload.class} (expected map)"
|
|
266
|
+
end
|
|
267
|
+
parsed = payload
|
|
268
|
+
raise RProcessError, parsed['message'] || parsed.inspect unless status == 'success'
|
|
269
|
+
|
|
270
|
+
parsed
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Schedule one REQ and invoke +block+ on a background thread when RET arrives (or on failure).
|
|
274
|
+
#
|
|
275
|
+
# Unlike +eval_r+, this method returns immediately with the +call_id+ String; it does not wait
|
|
276
|
+
# for R unless +timeout+ is nil (wait forever on the Ruby side for RET) or a Numeric (raise
|
|
277
|
+
# completion with {TimeoutError} if RET is late).
|
|
278
|
+
#
|
|
279
|
+
# @param timeout [nil, Numeric] +nil+ means no limit; a positive number starts a timer thread.
|
|
280
|
+
# @return [String] +call_id+ for correlation / debugging
|
|
281
|
+
# @raise [ArgumentError] if no block is given
|
|
282
|
+
def eval_r_async(code, session_id: 'default', instance_id: 'default', parent_id: nil, timeout: nil, &block)
|
|
283
|
+
raise ArgumentError, 'eval_r_async requires a block' unless block
|
|
284
|
+
|
|
285
|
+
call_id = SecureRandom.uuid
|
|
286
|
+
slot = AsyncPendingEntry.new(instance_id, block)
|
|
287
|
+
@pending_mx.synchronize { @pending[call_id] = slot }
|
|
288
|
+
|
|
289
|
+
req = Envelope.encode(
|
|
290
|
+
'call_id' => call_id,
|
|
291
|
+
'type' => 'REQ',
|
|
292
|
+
'session_id' => session_id,
|
|
293
|
+
'instance_id' => instance_id,
|
|
294
|
+
'parent_id' => parent_id,
|
|
295
|
+
'payload' => code
|
|
296
|
+
)
|
|
297
|
+
@write_mx.synchronize { Framing.write_frame(@sock, req) }
|
|
298
|
+
|
|
299
|
+
if timeout
|
|
300
|
+
slot.timer_thread = Thread.new { async_timeout_wait(call_id, slot, timeout) }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
call_id
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Register a Ruby callback for R->Ruby `CALL` envelopes.
|
|
307
|
+
#
|
|
308
|
+
# The gatekeeper will invoke this callback when it receives a `CALL` with the
|
|
309
|
+
# corresponding `callback_call_id`, and this client will reply with a `RET`.
|
|
310
|
+
# The same `call_id` may be used many times (R stub function is reused); the block
|
|
311
|
+
# stays registered until the client stops — do not delete on first CALL.
|
|
312
|
+
def register_callback(&block)
|
|
313
|
+
callback_call_id = SecureRandom.uuid
|
|
314
|
+
@callbacks_mx.synchronize { @callbacks[callback_call_id] = block }
|
|
315
|
+
callback_call_id
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
# Gatekeeper parses RET success payload with strtod. Contract:
|
|
321
|
+
# - `register_callback_proc_stub` ends with nil → ACK "1" (semantic value staged in R).
|
|
322
|
+
# - Direct `register_callback` blocks may return a Numeric scalar; pass it through for legacy tests.
|
|
323
|
+
def self.callback_success_transport_payload(result)
|
|
324
|
+
case result
|
|
325
|
+
when nil
|
|
326
|
+
'1'
|
|
327
|
+
when Numeric
|
|
328
|
+
x = result.to_f
|
|
329
|
+
(x.nan? || x.infinite?) ? '0' : x.to_s
|
|
330
|
+
when true
|
|
331
|
+
'1'
|
|
332
|
+
when false
|
|
333
|
+
'0'
|
|
334
|
+
else
|
|
335
|
+
'1'
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Emit debug logs when GALAAZ_DEBUG is set.
|
|
340
|
+
def debug_log(msg)
|
|
341
|
+
return unless ENV['GALAAZ_DEBUG']
|
|
342
|
+
STDERR.puts("[NewBridge::SessionClient] #{msg}")
|
|
343
|
+
STDERR.flush
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Wait for RET for one `call_id` by popping from queue `q`.
|
|
347
|
+
# On timeout it removes the pending queue and raises TimeoutError.
|
|
348
|
+
def wait_for_ret(q, call_id, timeout)
|
|
349
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
350
|
+
Timeout.timeout(timeout) do
|
|
351
|
+
v = q.pop
|
|
352
|
+
raise RProcessError, 'R connection closed' if v == :closed
|
|
353
|
+
|
|
354
|
+
v
|
|
355
|
+
end
|
|
356
|
+
rescue Timeout::Error
|
|
357
|
+
@pending_mx.synchronize { @pending.delete(call_id) }
|
|
358
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
359
|
+
raise TimeoutError, format('no RET for %<id>s (timeout=%<timeout>.3fs elapsed=%<elapsed>.3fs)', id: call_id, timeout: timeout.to_f, elapsed: elapsed)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Reader loop running in a background thread:
|
|
363
|
+
# - reads framed envelopes
|
|
364
|
+
# - dispatches by `type`
|
|
365
|
+
# - shields the main eval path from socket read latency
|
|
366
|
+
def reader_loop
|
|
367
|
+
loop do
|
|
368
|
+
bytes = Framing.read_frame(@sock)
|
|
369
|
+
h = Envelope.decode(bytes)
|
|
370
|
+
debug_log("RX type=#{h['type']} call_id=#{h['call_id']} session_id=#{h['session_id']} instance_id=#{h['instance_id']}") if h.is_a?(Hash)
|
|
371
|
+
case h['type']
|
|
372
|
+
when 'RET'
|
|
373
|
+
entry = @pending_mx.synchronize { @pending.delete(h['call_id']) }
|
|
374
|
+
case entry
|
|
375
|
+
when Queue
|
|
376
|
+
entry.push(h)
|
|
377
|
+
when AsyncPendingEntry
|
|
378
|
+
entry.cancel_timer!
|
|
379
|
+
Thread.new { deliver_eval_r_async_completion(entry, h) }
|
|
380
|
+
when nil
|
|
381
|
+
debug_log("stray RET call_id=#{h['call_id']}")
|
|
382
|
+
else
|
|
383
|
+
debug_log("unexpected pending type=#{entry.class} call_id=#{h['call_id']}")
|
|
384
|
+
end
|
|
385
|
+
when 'CALL'
|
|
386
|
+
handle_call(h)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
rescue Framing::TruncatedFrame, Framing::Error, IOError, Errno::ECONNRESET, Errno::EPIPE, Envelope::Error
|
|
390
|
+
signal_closed
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Handle one R->Ruby callback request:
|
|
394
|
+
# - look up the Ruby callback by `call_id`
|
|
395
|
+
# - execute it in a new thread
|
|
396
|
+
# - send RET back with status success/error
|
|
397
|
+
def handle_call(h)
|
|
398
|
+
call_id = h['call_id']
|
|
399
|
+
instance_id = h['instance_id'] || 'default'
|
|
400
|
+
payload = h['payload']
|
|
401
|
+
debug_log("CALL received call_id=#{call_id} instance_id=#{instance_id} payload=#{payload.inspect}")
|
|
402
|
+
|
|
403
|
+
callback = @callbacks_mx.synchronize { @callbacks[call_id] }
|
|
404
|
+
unless callback
|
|
405
|
+
send_ret(call_id: call_id, status: 'error', payload: "unknown callback #{call_id}", instance_id: instance_id)
|
|
406
|
+
return
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Phase 4: Run callback in separate thread so reader thread stays free
|
|
410
|
+
# for nested REQ/RET handling
|
|
411
|
+
Thread.new do
|
|
412
|
+
begin
|
|
413
|
+
result = callback.call(payload, call_id)
|
|
414
|
+
debug_log("CALL result call_id=#{call_id} result=#{result.inspect}")
|
|
415
|
+
payload_txt = self.class.callback_success_transport_payload(result)
|
|
416
|
+
send_ret(call_id: call_id, status: 'success', payload: payload_txt, instance_id: instance_id)
|
|
417
|
+
rescue IOError, Errno::EPIPE => e
|
|
418
|
+
# Socket closed - ignore, connection is shutting down
|
|
419
|
+
debug_log("CALL send failed (socket closed) call_id=#{call_id}: #{e.message}")
|
|
420
|
+
rescue => e
|
|
421
|
+
debug_log("CALL error call_id=#{call_id} error=#{e.class}: #{e.message}")
|
|
422
|
+
begin
|
|
423
|
+
send_ret(call_id: call_id, status: 'error', payload: e.message, instance_id: instance_id)
|
|
424
|
+
rescue IOError, Errno::EPIPE
|
|
425
|
+
# Socket closed - ignore
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Send RET envelope for a CALL.
|
|
432
|
+
# Payload is encoded as a simple string (`result.to_s` or exception message),
|
|
433
|
+
# and gatekeeper converts it back into its expected scalar type.
|
|
434
|
+
def send_ret(call_id:, status:, payload:, instance_id:)
|
|
435
|
+
debug_log("TX RET call_id=#{call_id} instance_id=#{instance_id} status=#{status} payload=#{payload.inspect}")
|
|
436
|
+
ret = Envelope.encode(
|
|
437
|
+
'call_id' => call_id,
|
|
438
|
+
'type' => 'RET',
|
|
439
|
+
'status' => status,
|
|
440
|
+
'payload' => payload,
|
|
441
|
+
'instance_id' => instance_id,
|
|
442
|
+
'parent_id' => nil
|
|
443
|
+
)
|
|
444
|
+
@write_mx.synchronize { Framing.write_frame(@sock, ret) }
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Signal all pending eval_r requests that the socket is closed,
|
|
448
|
+
# allowing waiting `eval_r` calls to raise promptly.
|
|
449
|
+
def signal_closed
|
|
450
|
+
entries = @pending_mx.synchronize do
|
|
451
|
+
e = @pending.dup
|
|
452
|
+
@pending.clear
|
|
453
|
+
e
|
|
454
|
+
end
|
|
455
|
+
entries.each do |_call_id, entry|
|
|
456
|
+
case entry
|
|
457
|
+
when Queue
|
|
458
|
+
entry.push(:closed)
|
|
459
|
+
when AsyncPendingEntry
|
|
460
|
+
entry.cancel_timer!
|
|
461
|
+
Thread.new { invoke_async_block_safe(entry, EvalResult.failure(RProcessError.new('R connection closed'))) }
|
|
462
|
+
else
|
|
463
|
+
debug_log("signal_closed: skip pending type=#{entry.class}")
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def ret_hash_to_eval_result(ret, expected_instance_id)
|
|
469
|
+
unless ret.is_a?(Hash)
|
|
470
|
+
return EvalResult.failure(RProcessError.new("invalid RET payload type=#{ret.class}"))
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if ret['instance_id'] && ret['instance_id'] != expected_instance_id
|
|
474
|
+
return EvalResult.failure(RProcessError.new("instance_id mismatch: expected=#{expected_instance_id} got=#{ret['instance_id']}"))
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
payload = ret['payload']
|
|
478
|
+
unless payload.is_a?(Hash)
|
|
479
|
+
return EvalResult.failure(RProcessError.new("invalid RET payload inner type=#{payload.class} (expected map)"))
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
return EvalResult.failure(RProcessError.new(payload['message'] || payload.inspect)) if ret['status'] != 'success'
|
|
483
|
+
|
|
484
|
+
EvalResult.success(payload)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def deliver_eval_r_async_completion(slot, ret_env)
|
|
488
|
+
result = ret_hash_to_eval_result(ret_env, slot.instance_id)
|
|
489
|
+
invoke_async_block_safe(slot, result)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def invoke_async_block_safe(slot, result)
|
|
493
|
+
slot.block.call(result)
|
|
494
|
+
rescue StandardError => e
|
|
495
|
+
debug_log("eval_r_async block raised #{e.class}: #{e.message}")
|
|
496
|
+
raise if ENV['GALAAZ_ASYNC_RAISE_COMPLETION']
|
|
497
|
+
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def async_timeout_wait(call_id, slot, timeout)
|
|
502
|
+
sleep(timeout.to_f)
|
|
503
|
+
timed_out = false
|
|
504
|
+
@pending_mx.synchronize do
|
|
505
|
+
if @pending[call_id].equal?(slot)
|
|
506
|
+
@pending.delete(call_id)
|
|
507
|
+
timed_out = true
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
return unless timed_out
|
|
511
|
+
|
|
512
|
+
invoke_async_block_safe(slot, EvalResult.failure(TimeoutError.new(
|
|
513
|
+
format('no RET for %<id>s (timeout=%<timeout>.3fs)', id: call_id, timeout: timeout.to_f)
|
|
514
|
+
)))
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def append_r_stderr(text)
|
|
518
|
+
@r_stderr_mx.synchronize do
|
|
519
|
+
@r_stderr << text.to_s
|
|
520
|
+
if @r_stderr.bytesize > @r_stderr_limit
|
|
521
|
+
@r_stderr = @r_stderr.byteslice(-@r_stderr_limit, @r_stderr_limit) || +''
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def current_r_stderr
|
|
527
|
+
@r_stderr_mx.synchronize { @r_stderr.to_s.dup.strip }
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require_relative 'framing'
|
|
5
|
+
|
|
6
|
+
module NewBridge
|
|
7
|
+
# Minimal TCP client/server for framed byte payloads (Phase 0).
|
|
8
|
+
module TcpFramed
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Yields bound port; server echoes one frame then closes.
|
|
14
|
+
def with_echo_server(host: '127.0.0.1')
|
|
15
|
+
server = TCPServer.new(host, 0)
|
|
16
|
+
port = server.addr[1]
|
|
17
|
+
thread = Thread.new do
|
|
18
|
+
client = server.accept
|
|
19
|
+
begin
|
|
20
|
+
payload = Framing.read_frame(client)
|
|
21
|
+
Framing.write_frame(client, payload)
|
|
22
|
+
ensure
|
|
23
|
+
client.close rescue nil
|
|
24
|
+
end
|
|
25
|
+
ensure
|
|
26
|
+
server.close rescue nil
|
|
27
|
+
end
|
|
28
|
+
yield port
|
|
29
|
+
ensure
|
|
30
|
+
thread&.join(5)
|
|
31
|
+
server&.close rescue nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def send_receive(host, port, payload)
|
|
35
|
+
sock = TCPSocket.new(host, port)
|
|
36
|
+
begin
|
|
37
|
+
Framing.write_frame(sock, payload)
|
|
38
|
+
Framing.read_frame(sock)
|
|
39
|
+
ensure
|
|
40
|
+
sock.close
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/new_bridge.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'new_bridge/envelope'
|
|
4
|
+
require_relative 'new_bridge/framing'
|
|
5
|
+
require_relative 'new_bridge/eval_result'
|
|
6
|
+
require_relative 'new_bridge/tcp_framed'
|
|
7
|
+
require_relative 'new_bridge/session_client'
|
|
8
|
+
require_relative 'new_bridge/instance_pool_client'
|
|
9
|
+
require_relative 'new_bridge/r_instance_manager'
|