View Source Venomous (Venomous v0.7.7)

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

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

Architecture

Venomous consists of several key components:

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

Summary

Functions

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

Link to this function

adopt_snake_pet(name, opts \\ [])

View Source
@spec adopt_snake_pet(name :: atom(), opts :: keyword()) ::
  {:error, any()} | {:ok, name :: atom()}

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.

Link to this function

get_snakes_ready(amount)

View Source
@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!/2 and retrieve_snake!/0 will loop until they are freed.

Parameters

  • amount of snakes to retrieve

Returns

@spec list_alive_snakes() :: [{pid(), pid(), non_neg_integer(), atom(), any()}]

Returns list of :ets table containing alive snakes

Link to this function

pet_snake_run(args, name, timeout \\ 15000)

View Source
@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

Link to this function

python(snake_args, opts \\ [])

View Source
@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

Opts

  • :python_timeout ms timeout. Kills python OS process on timeout. Default: 15_000
  • :kill_python_on_exception Should 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

Link to this function

python!(snake_args, opts \\ [])

View Source
@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_interval ms to wait before requesting snake again Default: 200
  • :python_timeout ms timeout. Kills python OS process on timeout. Default: 15_000
  • :kill_python_on_exception Should 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

Link to this function

retrieve_snake!(interval \\ 100)

View Source
@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

Link to this function

slay_pet_worker(name, termination_style \\ :peaceful)

View Source
@spec slay_pet_worker(name :: atom(), termination_style :: atom()) :: :ok

Kills the named python process :brutal also kills the OS process of python, ensuring the process does not continue execution.

Parameters

  • name atom
  • a Way to kill process. :brutal additionally kills with kill -9 ensuring the python does not execute further. Default: :peaceful

Returns

:ok

Link to this function

slay_python_worker(snake_worker, termination_style \\ :peaceful)

View Source
@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.SnakeWorker struct
  • a Way to kill process. :brutal additionally kills with kill -9 ensuring the python does not execute further. Default: :peaceful

Returns

:ok

Link to this function

snake_run(snake_args, worker, opts \\ [])

View Source
@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

Opts

  • :python_timeout ms timeout. Kills python OS process on timeout. Default: 15_000
  • :kill_python_on_exception Should 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