# Task 10: Async Execution Support **Phase**: 4 - Advanced Features **Priority**: Low **Estimated Time**: 3-4 hours **Dependencies**: Phase 1 Complete ## Objective Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs. ## Prerequisites - Phase 1 complete - Understanding of Emacs async processes ## Background Some Elixir operations can take a long time: - Database migrations - Large data processing - Network operations - Build tasks Async execution allows: - Continue editing while code runs - Visual indicator of running blocks - Cancel long-running operations ## Steps ### Step 1: Add async configuration Add to `ob-elixir.el`: ```elisp ;;; Async Configuration (defcustom ob-elixir-async-timeout 300 "Timeout in seconds for async execution. After this time, async execution will be cancelled." :type 'integer :group 'ob-elixir) (defvar ob-elixir--async-processes (make-hash-table :test 'equal) "Hash table mapping buffer positions to async processes.") (defconst org-babel-header-args:elixir '((mix-project . :any) (mix-env . :any) (remsh . :any) (node-name . :any) (node-sname . :any) (cookie . :any) (async . ((yes no)))) ; NEW: async execution "Elixir-specific header arguments.") ``` ### Step 2: Implement async execution ```elisp ;;; Async Execution (defun ob-elixir--execute-async (body result-type callback) "Execute BODY asynchronously. RESULT-TYPE is 'value or 'output. CALLBACK is called with the result when complete." (let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs")) (code (if (eq result-type 'value) (ob-elixir--wrap-for-value body) body)) (output-buffer (generate-new-buffer " *ob-elixir-async*")) process) ;; Write code to temp file (with-temp-file tmp-file (insert code)) ;; Start async process (setq process (start-process "ob-elixir-async" output-buffer ob-elixir-command tmp-file)) ;; Set up process sentinel (set-process-sentinel process (lambda (proc event) (when (memq (process-status proc) '(exit signal)) (let ((result (with-current-buffer (process-buffer proc) (buffer-string)))) ;; Clean up (kill-buffer (process-buffer proc)) (delete-file tmp-file) ;; Call callback with result (funcall callback (ob-elixir--process-result (string-trim result))))))) ;; Set up timeout (run-at-time ob-elixir-async-timeout nil (lambda () (when (process-live-p process) (kill-process process) (funcall callback "Error: Async execution timed out")))) process)) ``` ### Step 3: Integrate with org-babel ```elisp (defun ob-elixir--async-p (params) "Return t if PARAMS specify async execution." (string= "yes" (cdr (assq :async params)))) (defun org-babel-execute:elixir (body params) "Execute a block of Elixir code with org-babel." (let* ((session (cdr (assq :session params))) (result-type (cdr (assq :result-type params))) (result-params (cdr (assq :result-params params))) (async (ob-elixir--async-p params)) (full-body (org-babel-expand-body:generic body params (org-babel-variable-assignments:elixir params)))) (if async ;; Async execution (ob-elixir--execute-async-block full-body result-type params) ;; Sync execution (existing code) (let ((result (ob-elixir--execute-sync full-body result-type params))) (org-babel-reassemble-table (org-babel-result-cond result-params result (ob-elixir--table-or-string result)) (org-babel-pick-name (cdr (assq :colname-names params)) (cdr (assq :colnames params))) (org-babel-pick-name (cdr (assq :rowname-names params)) (cdr (assq :rownames params)))))))) (defun ob-elixir--execute-sync (body result-type params) "Execute BODY synchronously." (cond ((and (cdr (assq :session params)) (not (string= (cdr (assq :session params)) "none"))) (ob-elixir--evaluate-in-session (cdr (assq :session params)) body result-type params)) ((ob-elixir--resolve-mix-project params) (ob-elixir--execute-with-mix body result-type params)) (t (ob-elixir--execute body result-type)))) ``` ### Step 4: Implement async block handling ```elisp (defun ob-elixir--execute-async-block (body result-type params) "Execute BODY asynchronously and insert results when done." (let ((buffer (current-buffer)) (point (point)) (result-params (cdr (assq :result-params params))) (marker (copy-marker (point)))) ;; Show placeholder (ob-elixir--insert-async-placeholder marker) ;; Execute async (ob-elixir--execute-async body result-type (lambda (result) (ob-elixir--insert-async-result buffer marker result result-params params))) ;; Return placeholder message "Executing asynchronously...")) (defun ob-elixir--insert-async-placeholder (marker) "Insert a placeholder at MARKER indicating async execution." (save-excursion (goto-char marker) (end-of-line) (insert "\n") (insert "#+RESULTS:\n") (insert ": [Executing...]\n"))) (defun ob-elixir--insert-async-result (buffer marker result result-params params) "Insert RESULT at MARKER in BUFFER." (when (buffer-live-p buffer) (with-current-buffer buffer (save-excursion (goto-char marker) ;; Find and remove placeholder (when (search-forward ": [Executing...]" nil t) (beginning-of-line) (let ((start (point))) (forward-line 1) (delete-region start (point)))) ;; Insert real result (let ((formatted (org-babel-result-cond result-params result (ob-elixir--table-or-string result)))) (org-babel-insert-result formatted result-params)))))) ``` ### Step 5: Add cancellation support ```elisp (defun ob-elixir-cancel-async () "Cancel the async execution at point." (interactive) (let* ((pos (point)) (process (gethash pos ob-elixir--async-processes))) (if (and process (process-live-p process)) (progn (kill-process process) (remhash pos ob-elixir--async-processes) (message "Async execution cancelled")) (message "No async execution at point")))) (defun ob-elixir-cancel-all-async () "Cancel all running async executions." (interactive) (maphash (lambda (_pos process) (when (process-live-p process) (kill-process process))) ob-elixir--async-processes) (clrhash ob-elixir--async-processes) (message "All async executions cancelled")) ``` ### Step 6: Add visual indicators ```elisp (defface ob-elixir-async-running '((t :background "yellow" :foreground "black")) "Face for source blocks with running async execution." :group 'ob-elixir) (defun ob-elixir--highlight-async-block (start end) "Highlight the region from START to END as running." (let ((overlay (make-overlay start end))) (overlay-put overlay 'face 'ob-elixir-async-running) (overlay-put overlay 'ob-elixir-async t) overlay)) (defun ob-elixir--remove-async-highlight () "Remove async highlighting from current block." (dolist (ov (overlays-in (point-min) (point-max))) (when (overlay-get ov 'ob-elixir-async) (delete-overlay ov)))) ``` ### Step 7: Add tests Create `test/test-ob-elixir-async.el`: ```elisp ;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*- (require 'ert) (require 'ob-elixir) (ert-deftest ob-elixir-test-async-detection () "Test async header argument detection." (should (ob-elixir--async-p '((:async . "yes")))) (should-not (ob-elixir--async-p '((:async . "no")))) (should-not (ob-elixir--async-p '()))) (ert-deftest ob-elixir-test-async-execution () "Test async execution completion." (skip-unless (executable-find ob-elixir-command)) (let ((result nil) (done nil)) (ob-elixir--execute-async "1 + 1" 'value (lambda (r) (setq result r) (setq done t))) ;; Wait for completion (with-timeout (10 (error "Async test timed out")) (while (not done) (accept-process-output nil 0.1))) (should (equal "2" result)))) (ert-deftest ob-elixir-test-async-timeout () "Test async timeout handling." (skip-unless (executable-find ob-elixir-command)) (let ((ob-elixir-async-timeout 1) (result nil) (done nil)) (ob-elixir--execute-async ":timer.sleep(5000)" ; Sleep for 5 seconds 'value (lambda (r) (setq result r) (setq done t))) ;; Wait for timeout (with-timeout (3 (error "Test timed out")) (while (not done) (accept-process-output nil 0.1))) (should (string-match-p "timed out" result)))) (provide 'test-ob-elixir-async) ``` ### Step 8: Document usage Add to documentation: ```org * Async Execution ** Long-running computation #+BEGIN_SRC elixir :async yes # This won't block Emacs Enum.reduce(1..1000000, 0, &+/2) #+END_SRC ** Async with Mix project #+BEGIN_SRC elixir :async yes :mix-project ~/my_app MyApp.expensive_operation() #+END_SRC ** Cancel with M-x ob-elixir-cancel-async ``` ## Acceptance Criteria - [ ] `:async yes` executes code asynchronously - [ ] Placeholder shown while executing - [ ] Results inserted when complete - [ ] Timeout handled gracefully - [ ] `ob-elixir-cancel-async` cancels execution - [ ] Visual indicator for running blocks - [ ] All tests pass ## Limitations - Sessions cannot be async (they're inherently stateful) - Multiple async blocks may have ordering issues - Async results may not integrate perfectly with noweb ## Files Modified - `ob-elixir.el` - Add async support - `test/test-ob-elixir-async.el` - Add async tests ## References - [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html) - [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)