Tareas programadas en tu applicacion elixir o phoenix


Introducción

trickster Por si te lo preguntás, la imagen está pensada para representar al trickster, ya que planeo hacer muchas publicaciones sobre tips y trucos.


En este artículo exploraremos cómo ejecutar tareas programadas utilizando el recurso CronJob en Kubernetes. Podrías preguntarte cuándo usar esto en lugar de crear tu propio gen_server y manejarlo desde la aplicación misma. La respuesta es bastante simple: cada vez que necesites ejecutar una tarea específica de manera repetitiva, por ejemplo: crear un respaldo diario, enviar estadísticas diarias, etc. Aún sería posible y completamente válido hacerlo con Elixir usando la app quantum-core (pista: posiblemente un próximo post trate sobre probar este enfoque 😉).


¿Cuándo usar cada opción? Principales diferencias:

Programador casero:

  • Flexible.
  • Adaptado a tus necesidades.
  • Puede requerir un deployment para cambiar/actualizar la programación o tareas.
  • Más carga sobre el BEAM, ya que más apps estarán corriendo.

Programador externo (cronjobs en k8s, cronjob en máquinas virtuales, etc.):

  • Ligeramente más lento en el arranque (similar a los “cold starts”, requiere más preparación).
  • Más fácil cambiar/actualizar la programación sin hacer un redeployment.
  • El registro y el historial se preservan fácilmente (y son configurables).
  • Historial y conservación de trabajos fácilmente configurables.
  • Más carga sobre el API server de k8s.

Librería o app (tipo cron, como quantum-core):

  • Muy similar a un cronjob regular.
  • Súper flexible.
  • Puede requerir un deployment para cambiar/actualizar la programación o tareas.
  • Más carga sobre el BEAM, ya que más apps estarán corriendo.

Algunas preguntas

Algunas preguntas que podrías hacerte antes de programar una tarea:

  • ¿Puede ejecutarse de manera concurrente de forma segura? ¿Qué pasa si la tarea anterior no terminó, por ejemplo?
  • ¿Con qué frecuencia necesita ejecutarse?
  • ¿Necesita alterar algún dato fuera de la tarea en sí? Por ejemplo, preparar algo previamente (script).

Y muchas más preguntas podrían surgir antes de crear una tarea programada, pero por ahora esto debería ser suficiente.


Escenarios

Tenemos dos cronjobs ejecutándose en diferentes momentos, pero exploraremos solo el segundo, ya que la configuración es muy similar entre ambos. Este cronjob hace un análisis de sentimientos en los comentarios usando Ollama, luego aprueba automáticamente aquellos comentarios que sean neutrales o positivos. El otro cronjob envía una notificación por correo electrónico diariamente a las 00:00 (hora del servidor) sobre las nuevas publicaciones a los suscriptores del blog (cuentas registradas), por si tenías curiosidad y querés echarle un vistazo.


Basta de preámbulos, vamos al grano. Hay cosas interesantes como el uso de concurrencyPolicy: Forbid, que le indica a Kubernetes que no queremos que otro pod reemplace o ejecute el cronjob de manera concurrente (las otras opciones son Allow y Replace). Como necesitamos iniciar nuestra aplicación, también necesitamos que algunos secretos estén presentes para poder enviar correos y conectarnos a la base de datos. El resto es bastante sencillo y no específico de esta tarea, excepto por el comando, que probablemente sea la parte más interesante. Al llamar a la release de la app con eval, podemos ejecutar nuestro módulo, en este caso, la función Tr.Tracker.start.


Nota: por defecto, los cronjobs conservan las últimas 3 ejecuciones exitosas y 1 fallida, configurable bajo las claves: .spec.successfulJobsHistoryLimit, .spec.failedJobsHistoryLimit.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: tr-approver
  namespace: tr
  labels:
    name: tr
spec:
  concurrencyPolicy: Forbid
  schedule: "00 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
          imagePullSecrets:
          - name: regcred
          containers:
          - name: tr
            image: kainlite/tr:master
            command:
              - /app/bin/tr
              - eval
              - Tr.Approver.start
            envFrom:
            - secretRef:
                name: tr-postgres-config
            - secretRef:
                name: tr-mailer-config
            env:
            - name: POD_IP
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: status.podIP
            securityContext:
              allowPrivilegeEscalation: false
            imagePullPolicy: Always
          restartPolicy: Never

Exploremos el código un poco. Este ejemplo es excelente para ver cómo iniciar las apps y usar el BEAM de manera casi “headless”, casi como una función Lambda si querés. Como notarás, el start asegura que nuestra app y todas las aplicaciones necesarias estén corriendo antes de hacer cualquier trabajo. Esto es necesario debido a cómo funcionan las apps en el BEAM. Básicamente, busca todos los comentarios que aún no han sido aprobados y los envía a Ollama. El modelo está configurado para responder: positivo, neutral o negativo dependiendo del texto en el comentario. Si considera que el comentario es aceptable, se aprueba automáticamente sin intervención humana; de lo contrario, queda sin aprobar. Hay varias mejoras que se pueden hacer aquí, pero por ahora hace el trabajo.


lib/tr/approver.ex

defmodule Tr.Approver do
  @moduledoc """
  Tarea básica para aprobar comentarios si pasan el análisis de sentimiento
  """
  @app :tr

  defp load_app do
    Application.load(@app)
  end

  defp start_app do
    load_app()
    Application.ensure_all_started(@app)
  end

  @doc """
  Si la llama está de acuerdo con el sentimiento, entonces el comentario puede ser aprobado automáticamente
  """
  def check_comment_sentiment(comment) do
    ollama_sentiment = Tr.Ollama.send(comment.body)

    ollama_sentiment
  end

  def start do
    start_app()

    comments = Tr.Post.get_unapproved_comments()

    Enum.each(comments, fn comment ->
      if check_comment_sentiment(comment) do
        Tr.Post.approve_comment(comment)
      end
    end)
  end
end

¿Curioso sobre cómo interactuar con Ollama desde Elixir?

lib/tr/ollama.ex

defmodule Tr.Ollama do
  @moduledoc """
  FROM orca-mini

  PARAMETER temperature 0.2

  SYSTEM Eres un analizador de sentimientos. Recibirás un texto y solo deberás devolver una palabra: POSITIVE, NEGATIVE o NEUTRAL, dependiendo del sentimiento del texto.
  """

  def send(message) do
    api = api()
    p = %Ollamex.PromptRequest{model: "sentiments:latest", prompt: "MESSAGE " <> message}

    case Ollamex.generate_with_timeout(p, api) do
      {:error, :timeout} -> false
      {:ok, r} -> parse(r.response)
    end
  end

  defp api do
    Ollamex.API.new(System.get_env("OLLAMA_ENDPOINT", "http://localhost:11434/api"))
  end

  defp parse(r) do
    clean = r |> String.downcase() |> String.trim()

    clean =
      cond do
        String.contains?(clean, ":") ->
          String.split(clean, ":") |> List.last() |> String.trim()

        String.contains?(clean, ".") ->
          String.split(clean, ".") |> List.first() |> String.trim()
      end

    case clean do
      "neutral" -> true
      "positive" -> true
      "negative" -> false
    end
  end
end

El código es muy simple. Al leerlo ahora, después de un tiempo, veo muchas mejoras y refactorizaciones que se podrían hacer, ¡pero eso será para un futuro episodio 😆!


¿Tenés alguna pregunta? Dejá un comentario 👇, es posible que tengas que esperar 60 minutos para que la tarea se ejecute antes de que puedas leerla 😄, ¡pero al menos ahora ya sabés cómo funciona!

Errata

Si encontrás algún error o tenés alguna sugerencia, mandame un mensaje para que se pueda corregir.

También podés revisar el código fuente y los cambios en los sources aquí



No tienes cuenta? Regístrate aqui

Ya registrado? Iniciar sesión a tu cuenta ahora.

Iniciar session con GitHub
Iniciar sesion con Google
  • Comentarios

    Online: 0

Por favor inicie sesión para poder escribir comentarios.

by Gabriel Garrido