10 KiB
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 yesexecutes code asynchronously- Placeholder shown while executing
- Results inserted when complete
- Timeout handled gracefully
ob-elixir-cancel-asynccancels 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 supporttest/test-ob-elixir-async.el- Add async tests