docs and tasks
This commit is contained in:
358
tasks/10-async-execution.md
Normal file
358
tasks/10-async-execution.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user