View Source Venomous (Venomous v1.0.0)
A wrapper around erlport python Ports, designed to simplify concurrent use. It focuses dynamic extensibility, like spawning, reusing and killing processes on demand. Furthermore, unused processes get automatically cleaned up by a scheduled process which can be configured inside config.exs. Venomous core functions capture and handle :EXIT calls ensuring that all python process die with it and do not continue their execution.
The core concept revolves around "Snakes" which represent Python worker processes. These Venomous.SnakeWorker are managed and
supervised with Venomous.SnakeManager GenServer to allow concurrent and efficient execution of Python code. The Snakes pids
and python pids are stored inside :ets table and the Processes are handled by DymanicSupervisor called Venomous.SnakeSupervisor.
The unused Snakes get automatically killed by SnakeManager depending on the given configuration.
You can checkout examples here
Be sure to also check the README
Main Functionality
Basic processes
These are automatically managed and made for concurrent operations
python/2|python/1: The primary function to execute a Python function. IfVenomous.SnakeWorkeris free it retrieves it and runs the specifiedVenomous.SnakeArgsreturning the result/errorpython!/2|python!/1: Will wait until anyVenomous.SnakeWorkeris freed, requesting it with the given interval.
Named processes
Python processes with unique names not managed by Venomous.SnakeManager. These do not get cleaned-up and stay for as long as they are not killed
adopt_snake_pet/2: Creates a newVenomous.SnakeWorkerwith a name insideVenomous.PetSnakeSupervisorpet_snake_run/3: Runs givenVenomous.SnakeArgsinside the named python process
Architecture
Venomous consists of several key components:
Venomous.SerpentWatcher: Manages hot reloading.Venomous.SnakeWorker: Manages the execution of Python processes.Venomous.SnakeSupervisor: A DynamicSupervisor that oversees the SnakeWorkers.Venomous.SnakeManager: A GenServer that coordinates the SnakeWorkers and handles operations like spawning, retrieval and cleanup.Venomous.PetSnakeSupervisor: Similar to SnakeSupervisor but for named processes.Venomous.PetSnakeManager: Manages named python processes calls
Configuration Options
Venomous
# The way to kill python process on an OS level. :polite for SIGTERM | :brutal for SIGKILL. Anything else does not run kill
:venomous, :termination_style, :polite SnakeManager
The behavior and management of Snakes can be configured through the following options:
:venomous, :snake_manager, %{
snake_ttl_minutes: non_neg_integer(), # Time-to-live for a Snake in minutes. Default is 15 min.
perpetual_workers: non_neg_integer(), # Number of Snakes to keep alive perpetually. Default is 10.
cleaner_interval: non_neg_integer(), # Interval in milliseconds for cleaning up inactive Snakes. Default is 60_000 ms.
erlport_encoder: %{module: atom(), func: atom(), args: list(any())}, # Optional :erlport encoder/decoder python function for converting types. This function is applied to every unnamed python process started by SnakeManager. For more information see [Handling Erlport API](PYTHON.md)
}SnakeManager options
All of these are optional. However you will most likely want to set module_paths for python processes
config :venomous, :snake_manager, %{
# Optional :erlport encoder/decoder for type conversion between elixir/python applied to all workers. The function may also include any :erlport callbacks from python api
erlport_encoder: %{
module: :my_encoder_module,
func: :encode_my_snakes_please,
args: []
},
python_opts: [
module_paths: [], # List of paths to your python module files.
cd: "", # Change python's directory on spawn. Default is $PWD
compressed: 0, # Can be set from 0-9. May affect performance. Read more on [Erlport documentation](http://erlport.org/docs/python.html#erlang-api)
envvars: [], # additional python process envvars
packet_bytes: 4, # Size of erlport python packet. Default: 4 = max 4GB of data. Can be set to 1 = 256 bytes or 2 = ? bytes if you are sure you won't be transfering a lot of data.
python_executable: "" # path to python executable to use. defaults to PATH
]
# TTL whenever python process is inactive. Default: 15
snake_ttl_minutes: 15,
# Number of python workers that don't get cleared by SnakeManager when their TTL while inactive ends. Default: 10
perpetual_workers: 10,
# Interval for killing python processes past their ttl while inactive. Default: 60_000ms (1 min)
cleaner_interval: 60_000,
# reload module for hot reloading.
# default is already provided inside venomous python/ directory
reload_module: :reload,
}Hot reloading
Requires watchdog python module, which can be installed with mix venomous.watchdog install.
Only files inside module_paths config are being watched.
config :venomous, :serpent_watcher, [
enable: false,
logging: true, # logs every hot reload
module: :serpent_watcher, # Provided by default
func: :watch_directories, # Provided by default
manager_pid: Venomous.SnakeManager, # Provided by default
]Struct/Class comp
Venomous provides an easy way to convert structs into classes and back with VenomousTrait class and mix venomous.structs ... task.
$ mix venomous.structs
Simple utility to create python elixir compatible classes.
VenomousTrait class provides 2 functions:
- def from_dict(cls, erl_map: Map | Dict, structs: Dict = {}) -> cls
# converts Erlport Map or a Dict into the object class
- def into_erl(self) -> Map
# returns erlang compatible struct from self
To create basic python classes and encode/decode functions based on structs:
- mix venomous.structs MyModule.MyStruct MyModule.MoreStructs ...
To create extended classes depending on existing python class:
- mix venomous.structs MyModule.MyStruct:PythonClassName ...
To create for all available structs inside an application
- mix venomous.structs all my_application
You can see this used in the struct_test.exs and test_venomous.py
Auxiliary Functions
list_alive_snakes/0: Returns a list of :ets table containing currently alive Snakes.clean_inactive_snakes/0: Manually clears inactive Snakes depending on their ttl and returns the number of Snakes cleared.slay_python_worker/2: Kills a specified Python worker process and its SnakeWorker. :brutal or :polite can be specified as option, which willkill -9orkill -15the os process of python which prevents the code from executing until it finalizes or goes through iteration.slay_pet_worker/2: Kills a named Python processretrieve_snake/0: Retrieves aVenomous.SnakeWorkerand sets its status to :retrievedget_snakes_ready/1: Retrieves given amount ofVenomous.SnakeWorkers
Summary
Functions
Creates a named Venomous.SnakeWorker inside Venomous.PetSnakeSupervisor
Clears inactive snakes manually, returns number of snakes cleared.
Retrieves x amount of ready snakes and sets their status to :retrieved. In case of hitting max_children cap, stops and returns all available snakes.
Returns list of :ets table containing alive snakes
Used to run given Venomous.SnakeArgs inside named snake
Does not handle :EXIT signals like snake_run/2 does.
If pet snake with name does not exist will return :not_found
Preloads amount of workers with :ready state
Wrapper for calling python process
Tries to retrieve Venomous.SnakeWorker which then runs the given Venomous.SnakeArgs. In case of failure will return {:retrieve_error, message}.
In case :EXIT happens, it will kill python os process along with its worker and exit(reason)
If no Snake is available will continue requesting it with the given interval until any gets freed or receives :EXIT signal
Retrieves Venomous.SnakeWorker struct and sets it's status to :retrieved preventing other processes from accessing it.
If all processes are busy and exceeds max_children will return {:retrieve_error, message}.
If all processes are busy and exceeds max_children will wait for interval ms and try again. Traps the exit signals, to safely escape loop.
Kills the named python process :brutal also kills the OS process of python, ensuring the process does not continue execution.
Kills python process and its SnakeWorker :brutal also kills the OS process of python, ensuring the process does not continue execution.
Runs Venomous.SnakeArgs inside given Venomous.SnakeWorker.
Traps exit and awaits signals [:SNAKE_DONE, :SNAKE_ERROR, :EXIT]
In case of an exit, brutally kills the python process ensuring it doesn't get executed any further.
Functions
Creates a named Venomous.SnakeWorker inside Venomous.PetSnakeSupervisor
Parameters
- an atom() name.
- opts for python process
Options
Python options can be configured inside :venomous :python_opts config key
All of these are optional. However you will most likely want to set module_paths
erlport_encoder: %{module: atom(), func: atom(), args: list(any())}: Optional :erlport encoder/decoder python function for converting types. This function is applied to every unnamed python process started by SnakeManager. For more information see Handling Erlport API@available_opts [ :module_paths, # List of paths to your python modules :cd, # Change python's directory on spawn. Default is $PWD :compressed, # Can be set from 0-9. May affect performance. Read more on [Erlport documentation](http://erlport.org/docs/python.html#erlang-api) :envvars, # additional python process envvars :packet_bytes, # Size of erlport python packet. Default: 4 = max 4GB of data. Can be set to 1 = 256 bytes or 2 = ? bytes if you are sure you won't be transfering a lot of data. :python_executable # path to python executable to use. ]Returns
- :ok, name - in case of success
- :error, message - in case of failure
@spec clean_inactive_snakes() :: non_neg_integer()
Clears inactive snakes manually, returns number of snakes cleared.
@spec get_snakes_ready(non_neg_integer()) :: [Venomous.SnakeWorker.t()]
Retrieves x amount of ready snakes and sets their status to :retrieved. In case of hitting max_children cap, stops and returns all available snakes.
Warning
In case of retrieving all available snakes and not using them right away, functions like
python!/2andretrieve_snake!/0will loop until they are freed.
Parameters
- amount of snakes to retrieve
Returns
- A list of
Venomous.SnakeWorkerstructs
@spec list_alive_snakes() :: [{pid(), pid(), non_neg_integer(), atom(), any()}]
Returns list of :ets table containing alive snakes
@spec pet_snake_run(Venomous.SnakeArgs.t(), name :: atom(), timeout()) :: any() | :not_found | {:error, :timeout}
Used to run given Venomous.SnakeArgs inside named snake
Does not handle :EXIT signals like snake_run/2 does.
If pet snake with name does not exist will return :not_found
Preloads amount of workers with :ready state
@spec python( Venomous.SnakeArgs.t(), keyword() ) :: any()
Wrapper for calling python process
Tries to retrieve Venomous.SnakeWorker which then runs the given Venomous.SnakeArgs. In case of failure will return {:retrieve_error, message}.
In case :EXIT happens, it will kill python os process along with its worker and exit(reason)
Parameters
Venomous.SnakeArgsstruct of :module, :func, :args- opts \ []
Opts
:python_timeoutms timeout. Kills python OS process on timeout. Default: 15_000:kill_python_on_exceptionShould python process be killed on exception. Should be set to true if your python process exits by itself. Default: false
Returns
any() | {:error, :timeout} | {:killed, reason} | {retrieve_error: any()} retrieves output of python function or error
@spec python!( Venomous.SnakeArgs.t(), keyword() ) :: any()
If no Snake is available will continue requesting it with the given interval until any gets freed or receives :EXIT signal
Opts
:retrieve_intervalms to wait before requesting snake again Default: 200:python_timeoutms timeout. Kills python OS process on timeout. Default: 15_000:kill_python_on_exceptionShould python process be killed on exception. Should be set to true if your python process exits by itself. Default: false
@spec retrieve_snake() :: {:retrieve_error, reason :: term()} | Venomous.SnakeWorker.t()
Retrieves Venomous.SnakeWorker struct and sets it's status to :retrieved preventing other processes from accessing it.
If all processes are busy and exceeds max_children will return {:retrieve_error, message}.
Returns
Venomous.SnakeWorkerstruct. In case of error{:retrieve_error, message}
@spec retrieve_snake!(non_neg_integer()) :: Venomous.SnakeWorker.t()
If all processes are busy and exceeds max_children will wait for interval ms and try again. Traps the exit signals, to safely escape loop.
Parameters
- interval: The time to wait in milliseconds before retrying. Default is
@wait_for_snake_interval.
Returns
Venomous.SnakeWorkerstruct.
Kills the named python process :brutal also kills the OS process of python, ensuring the process does not continue execution.
Parameters
nameatom- a Way to kill process. :brutal additionally kills with kill -9 ensuring the python does not execute further. Default: :peaceful
Returns
:ok
@spec slay_python_worker(Venomous.SnakeWorker.t(), termination_style :: atom()) :: :ok
Kills python process and its SnakeWorker :brutal also kills the OS process of python, ensuring the process does not continue execution.
Parameters
Venomous.SnakeWorkerstruct- a Way to kill process. :brutal additionally kills with kill -9 ensuring the python does not execute further. Default: :peaceful
Returns
:ok
@spec snake_run(Venomous.SnakeArgs.t(), Venomous.SnakeWorker.t(), keyword()) :: any()
Runs Venomous.SnakeArgs inside given Venomous.SnakeWorker.
Traps exit and awaits signals [:SNAKE_DONE, :SNAKE_ERROR, :EXIT]
In case of an exit, brutally kills the python process ensuring it doesn't get executed any further.
Parameters
Venomous.SnakeArgsstruct of :module, :func, :argsVenomous.SnakeWorkerstruct- opts Keywords
Opts
:python_timeoutms timeout. Kills python OS process on timeout. Default: 15_000:kill_python_on_exceptionShould python process be killed on exception. Should be set to true if your python process exits by itself. Default: false
Returns
any() | {:error, :timeout} | {:killed, reason} | %SnakeError{} - retrieves output of python function or error