mafredri / zsh-async

Because your terminal should be able to perform tasks asynchronously without external tools!
MIT License
766 stars 34 forks source link

Changing a variable in a worker #4

Closed ayyess closed 8 years ago

ayyess commented 8 years ago

Is it possible to change a variable used by jobs in a worker? It seems that a possible way is to stop a worker and start it up again. The use case is a variable changes outside the worker and the worker needs to be able to read the new value. The code below shows that assignments don't stick between jobs

#!/bin/zsh

source ./async.zsh
async_init

set_val() {
    val=$1
    echo "set val: $val"
}

view_val() {
    echo "view val: $val"
}

async_start_worker my_worker -n
async_register_callback my_worker completed_callback

# Create a callback function to process results
COMPLETED=0
completed_callback() {
    COMPLETED=$(( COMPLETED + 1 ))
    print $3
}

view_val
set_val 100
view_val

echo "======================"

async_job my_worker view_val
async_job my_worker set_val 200
async_job my_worker view_val

while (( COMPLETED < 3 )); do
    print "Waiting..."
    sleep 0.1
done

echo "======================"

COMPLETED=0
async_stop_worker my_worker -n
val=300
async_start_worker my_worker -n
async_register_callback my_worker completed_callback
async_job my_worker view_val

while (( COMPLETED < 1 )); do
    print "Waiting..."
    sleep 0.1
done

Displays:

view val:
set val: 100
view val: 100
======================
Waiting...
view val:
set val: 200
view val:
======================
Waiting...
view val: 300
mafredri commented 8 years ago

The reason you're not seeing val inside your worker is that each worker only has access to the global environment as it is when it was started, zpty instances essentially create a copy of the current state, so updates are not propagated.

The way I recommend using async is to have your job functions accept and return parameters (this still means there's no way to communicate to a running job, other than to kill it).

  1. Set function prints the newly set value
  2. Callback function sees that the set function has finished, and updates the variable on the global scope
  3. New value is passes to the view job through a parameter

I've updated your example slightly to illustrate this:

#!/usr/bin/env zsh

source ./async.zsh
async_init

set_val() {
    val=$1
    print $val
}

view_val() {
    local val=$1
    echo "view val: $val"
}

async_start_worker my_worker -n
async_register_callback my_worker completed_callback

# Create a callback function to process results
COMPLETED=0
completed_callback() {
    case $1 in
        set_val) val=$3;; # set val to the output of set_val
    esac
    COMPLETED=$(( COMPLETED + 1 ))
    print $3
}

set_val 100

async_job my_worker view_val $val
async_job my_worker set_val 200

sleep 0.1 # waiting for set_val to return so that global var gets set

async_job my_worker view_val $val

while (( COMPLETED < 3 )); do
    print "Waiting..."
    sleep 0.1
done

Does this help with your problem? You should essentially synchronize your application through the callback function which is provided with information about which job has completed.

mafredri commented 8 years ago

Is this still relevant to you @andjscott? I thought about this some more and think that it can indeed be a valid feature to change the environment inside the worker.

A change along these lines in async.zsh could take care of it: https://github.com/mafredri/zsh-async/commit/fec46cfa7f3b12d09f19809bb492144f05436298

I'm not yet married to the API, and need to think it through some more. There is definitely potential for hidden user error in the above commit.

With this change, you could modify the code example you provided like so:

--- change_var.zsh.orig 2016-07-12 00:59:19.000000000 +0300
+++ change_var.zsh  2016-07-12 00:59:47.000000000 +0300
@@ -5,7 +5,6 @@

 set_val() {
     val=$1
-    echo "set val: $val"
 }

 view_val() {
@@ -30,10 +29,10 @@
 echo "======================"

 async_job my_worker view_val
-async_job my_worker set_val 200
+async_exec my_worker set_val 200
 async_job my_worker view_val

-while (( COMPLETED < 3 )); do
+while (( COMPLETED < 2 )); do
     print "Waiting..."
     sleep 0.1
 done

Of couse, the async_exec is really flexible in this case, you could even just write async_exec my_worker "typeset val=200" (no function).

Potentially we should limit this to only environment variables, and do some input validation based on that constraint. In this case the function signature should probably be something along these lines: async_set_env my_worker test=123 foo=bar.

ayyess commented 8 years ago

Yes that's very interesting. I'm using zsh-async for generating the PROMPT asynchronously. My workaround to updating a variable is to stop and start the worker.

mafredri commented 8 years ago

I've given this some more thought and decided I will not implement it. Allowing this kind of behavior would provide an awkward API at best and with the release of v1.3.0 passing arguments to a function no longer has any limitations.

This means you should not rely on any kind of global state, parameters can, and should, be passed to the command/function during the async_job call. The worker is a completely separated environment (kind of like web workers, for example) and should be treated as such. Relying on the state within a worker will only lead to headache, so it's best to think of it as not having any state.

Although, I might reconsider this, provided some good arguments in its favor.