Files
ob-elixir/tasks/10-async-execution.md

10 KiB

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:

;;; 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

;;; 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

(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

(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

(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

(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:

;;; 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:

* 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