Tareas programadas en tu applicacion elixir o phoenix
Introducción
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.
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?
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.
-
Comentarios
Online: 0
Por favor inicie sesión para poder escribir comentarios.