1212import errno
1313import json
1414import os
15+ from abc import ABC , abstractmethod
1516from typing import Any , Mapping
1617
18+ import flux .constants
1719from _flux ._core import ffi , lib
1820from flux .future import Future
1921from flux .rpc import RPC
@@ -31,19 +33,24 @@ class KVSWrapper(Wrapper):
3133RAW .flux_kvsitr_next .set_error_check (lambda x : False )
3234
3335
36+ def _get_value (valp ):
37+ try :
38+ ret = json .loads (ffi .string (valp [0 ]).decode ("utf-8" ))
39+ except json .decoder .JSONDecodeError :
40+ ret = ffi .string (valp [0 ]).decode ("utf-8" )
41+ except UnicodeDecodeError :
42+ ret = ffi .string (valp [0 ])
43+ return ret
44+
45+
3446def get_key_direct (flux_handle , key , namespace = None ):
3547 valp = ffi .new ("char *[1]" )
3648 future = RAW .flux_kvs_lookup (flux_handle , namespace , 0 , key )
3749 RAW .flux_kvs_lookup_get (future , valp )
3850 if valp [0 ] == ffi .NULL :
3951 return None
4052
41- try :
42- ret = json .loads (ffi .string (valp [0 ]).decode ("utf-8" ))
43- except json .decoder .JSONDecodeError :
44- ret = ffi .string (valp [0 ]).decode ("utf-8" )
45- except UnicodeDecodeError :
46- ret = ffi .string (valp [0 ])
53+ ret = _get_value (valp )
4754 RAW .flux_future_destroy (future )
4855 return ret
4956
@@ -773,3 +780,138 @@ def walk(directory, topdown=False, flux_handle=None, namespace=None):
773780 raise ValueError ("If directory is a key, flux_handle must be specified" )
774781 directory = KVSDir (flux_handle , directory , namespace = namespace )
775782 return _inner_walk (directory , "" , topdown , namespace = namespace )
783+
784+
785+ class WatchImplementation (Future , ABC ):
786+ """
787+ Interface for KVS based watchers
788+
789+ Users to implement watch_get() and watch_cancel() functions.
790+ """
791+
792+ def __del__ (self ):
793+ if self .needs_cancel is not False :
794+ self .cancel ()
795+ try :
796+ super ().__del__ ()
797+ except AttributeError :
798+ pass
799+
800+ def __init__ (self , future_handle ):
801+ super ().__init__ (future_handle )
802+ self .needs_cancel = True
803+
804+ @abstractmethod
805+ def watch_get (self , future ):
806+ pass
807+
808+ @abstractmethod
809+ def watch_cancel (self , future ):
810+ pass
811+
812+ def get (self , autoreset = True ):
813+ """
814+ Return the new value or None if the stream has terminated.
815+
816+ The future is auto-reset unless autoreset=False, so a subsequent
817+ call to get() will try to fetch the next value and thus
818+ may block.
819+ """
820+ try :
821+ # Block until Future is ready:
822+ self .wait_for ()
823+ ret = self .watch_get (self .pimpl )
824+ except OSError as exc :
825+ if exc .errno == errno .ENODATA :
826+ self .needs_cancel = False
827+ return None
828+ # raise handle exception if there is one
829+ self .raise_if_handle_exception ()
830+ # re-raise all other exceptions
831+ #
832+ # Note: overwrite generic OSError strerror string with the
833+ # EventWatch future error string to give the caller appropriate
834+ # detail (e.g. instead of "No such file or directory" use
835+ # "job <jobid> does not exist"
836+ #
837+ exc .strerror = self .error_string ()
838+ raise
839+ if autoreset is True :
840+ self .reset ()
841+ return ret
842+
843+ def cancel (self , stop = False ):
844+ """Cancel a streaming future
845+
846+ If stop=True, then deactivate the multi-response future so no
847+ further callbacks are called.
848+ """
849+ self .watch_cancel (self .pimpl )
850+ self .needs_cancel = False
851+ if stop :
852+ self .stop ()
853+
854+
855+ class KVSWatchFuture (WatchImplementation ):
856+ """
857+ A future returned from kvs_watch_async().
858+ """
859+
860+ def __init__ (self , future_handle ):
861+ super ().__init__ (future_handle )
862+
863+ def watch_get (self , future ):
864+ """
865+ Implementation of watch_get() for KVSWatchFuture.
866+
867+ Will be called from WatchABC.get()
868+ """
869+ valp = ffi .new ("char *[1]" )
870+ RAW .flux_kvs_lookup_get (future , valp )
871+ return _get_value (valp )
872+
873+ def watch_cancel (self , future ):
874+ """
875+ Implementation of watch_cancel() for KVSWatchFuture.
876+
877+ Will be called from WatchABC.cancel()
878+ """
879+ RAW .flux_kvs_lookup_cancel (future )
880+
881+
882+ def kvs_watch_async (
883+ flux_handle , key , namespace = None , waitcreate = False , uniq = False , full = False
884+ ):
885+ """Asynchronously get KVS updates for a key
886+
887+ Args:
888+ flux_handle: A Flux handle obtained from flux.Flux()
889+ key: the key on which to watch
890+ namespace: namespace to read from, defaults to None. If namespace
891+ is None, the namespace specified in the FLUX_KVS_NAMESPACE
892+ environment variable will be used. If FLUX_KVS_NAMESPACE is not
893+ set, the primary namespace will be used.
894+ waitcreate: If True and a key does not yet exist, will wait
895+ for it to exit. Defaults to False.
896+ uniq: If True, only different values will be returned by
897+ watch. Defaults to False.
898+ full: If True, any change that can affect the key is
899+ monitored. Typically, this is to capture when a parent directory
900+ is removed or altered in some way. Typically kvs watch will not
901+ detect this as the exact key has not been changed. Defaults to
902+ False.
903+
904+ Returns:
905+ KVSWatchFuture: a KVSWatchFuture object. Call .get() from the then
906+ callback to get the currently returned value from the Future object.
907+ """
908+
909+ flags = flux .constants .FLUX_KVS_WATCH
910+ if waitcreate :
911+ flags |= flux .constants .FLUX_KVS_WAITCREATE
912+ if uniq :
913+ flags |= flux .constants .FLUX_KVS_WATCH_UNIQ
914+ if full :
915+ flags |= flux .constants .FLUX_KVS_WATCH_FULL
916+ future = RAW .flux_kvs_lookup (flux_handle , namespace , flags , key )
917+ return KVSWatchFuture (future )
0 commit comments