Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move from deferred.el to another asynchronous execution framework #159

Open
telotortium opened this issue Aug 14, 2021 · 2 comments · May be fixed by #160
Open

Move from deferred.el to another asynchronous execution framework #159

telotortium opened this issue Aug 14, 2021 · 2 comments · May be fixed by #160
Labels
enhancement New feature or request

Comments

@telotortium
Copy link
Collaborator

I want to accomplish several things with this:

  • Useful stack traces - the stack traces from deferred.el code are generally useless, which makes it hard to debug issues in this library.
  • Avoiding stack overflow - if you want to defer a long chain of requests the straightforward way (using either recursive deferred chains or deferred:loop) there is no tail call elimination, so eventually you will stack overflow. I got around this by throwing and catching errors (see org-gcal--sync-buffer-inner from Enable entries to be managed either by Org or Google Calendar #129), but this also makes the stack hard to follow.
@telotortium telotortium added the enhancement New feature or request label Aug 14, 2021
@telotortium
Copy link
Collaborator Author

emacs-aio looks promising - see https://nullprogram.com/blog/2019/03/10/. Unfortunately, you can't fully preserve stack traces in Emacs, but perhaps this will still make the code easier to read and not prone to stack overflows.

@telotortium telotortium linked a pull request Aug 16, 2021 that will close this issue
@telotortium
Copy link
Collaborator Author

I've decided to go with emacs-aio - since it uses the Emacs generator framework, code written using this framework looks just like async/await code in many modern languages, so it will be a lot easier to read than others. I've decided to go with emacs-aio instead of emacs-async-await because it uses Emacs errors instead of a separate reject callback for the error case, which I think it easier to read and preserves some semblance of a stack trace. I'm particularly impressed by the easy imperative code pattern:

(aio-defun org-gcal--get-event-aio (calendar-id event-id)
  "Retrieves a Google Calendar event given a CALENDAR-ID and EVENT-ID.

Refreshes the access token if needed.
Returns an ‘aio-promise’ for a ‘request-response' object."
  (let* ((a-token (org-gcal--get-access-token))
         (response
          (aio-await
           (org-gcal--aio-request
            (concat
             (org-gcal-events-url calendar-id)
             (concat "/" event-id))
            :type "GET"
            :headers
            `(("Accept" . "application/json")
              ("Authorization" . ,(format "Bearer %s" a-token)))
            :parser 'org-gcal--json-read)))
         (_data (request-response-data response))
         (status-code (request-response-status-code response))
         (error-thrown (request-response-error-thrown response)))
        (cond
         ;; If there is no network connectivity, the response will not
         ;; include a status code.
         ((eq status-code nil)
          (org-gcal--notify
           "Got Error"
           "Could not contact remote service. Please check your network connectivity.")
          (error "Network connectivity issue"))
         ((eq 401 (or (plist-get (plist-get (request-response-data response) :error) :code)
                      status-code))
          (org-gcal--notify
           "Received HTTP 401"
           "OAuth token expired. Now trying to refresh token.")
          (aio-await (org-gcal--refresh-token-aio))
          (aio-await (org-gcal--get-event-aio calendar-id event-id)))
         ;; Generic error-handler meant to provide useful information about
         ;; failure cases not otherwise explicitly specified.
         ((not (eq error-thrown nil))
          (org-gcal--notify
           (concat "Status code: " (number-to-string status-code))
           (pp-to-string error-thrown))
          (error "Got error %S: %S" status-code error-thrown))
         ;; Fetch was successful.
         (t response))))

(aio-defun org-gcal--refresh-token-aio ()
  "Refresh OAuth access and return the new access token as a deferred object."
  (let* ((response
          (aio-await
           (org-gcal--aio-request
            org-gcal-token-url
            :type "POST"
            :data `(("client_id" . ,org-gcal-client-id)
                    ("client_secret" . ,org-gcal-client-secret)
                    ("refresh_token" . ,(org-gcal--get-refresh-token))
                    ("grant_type" . "refresh_token"))
            :parser 'org-gcal--json-read)))
         (data (request-response-data response))
         (status-code (request-response-status-code response))
         (error-thrown (request-response-error-thrown response)))
    (cond
     ((eq error-thrown nil)
      (plist-put org-gcal-token-plist
                 :access_token
                 (plist-get data :access_token))
      (org-gcal--save-sexp org-gcal-token-plist org-gcal-token-file)
      (let ((token (plist-get org-gcal-token-plist :access_token)))
        token))
     (t
      (error "Got error %S: %S" status-code error-thrown)))))

org-gcal--aio-request can be found in #160, where I'm doing the aio rewrite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant