Browse Source

Merge pull request #28 from Move-System/feat/fila-prioridade-materializacao

Feat(Materializacao): laco renasce sozinho — autossupervisao pelo processo web + supervisor opcional AB#1480
pull/3858/head
Kemuel-sepulvida 3 weeks ago
committed by GitHub
parent
commit
efdab6cb02
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      knowledge
  2. 82
      sapl/integracao_hub/management/commands/materializar_pdfs_para_assinatura.py
  3. 106
      sapl/integracao_hub/middleware.py
  4. 25
      sapl/integracao_hub/migrations/0007_batimento_laco.py
  5. 44
      sapl/integracao_hub/models.py
  6. 9
      sapl/integracao_hub/painel.py
  7. 22
      sapl/integracao_hub/templates/integracao_hub/painel_materializacao.html
  8. 161
      sapl/integracao_hub/tests/test_autossupervisao.py
  9. 10
      sapl/settings.py
  10. 5
      scripts/atualizar_producao.sh
  11. 35
      scripts/supervisor-materializacao.conf

2
knowledge

@ -1 +1 @@
Subproject commit f8c0df145abbb482cb80760a298f2bdff5e07578
Subproject commit 665a13d886cbac5ba96c50cb217cffc55a7fcb81

82
sapl/integracao_hub/management/commands/materializar_pdfs_para_assinatura.py

@ -10,7 +10,8 @@ from django.core.management.base import BaseCommand, CommandError
from django.db import IntegrityError, transaction
from django.utils import timezone
from sapl.integracao_hub.models import (DocumentoParaAssinatura,
from sapl.integracao_hub.models import (BatimentoLaco,
DocumentoParaAssinatura,
MateriaComFalhaMaterializacao,
MateriaParaMaterializar,
PassadaMaterializacao)
@ -177,6 +178,31 @@ class Command(BaseCommand):
if intervalo <= 0:
self._passada_registrada(somente_novos, disparada_por)
return
# Instância única do LAÇO (ADR 0015): batimento fresco de outro
# processo = já existe laço vivo, este sai. O lock de passada impedia
# conversão dupla; isto impede dois laços residentes disputando — o
# cenário que a autossupervisão cria quando dois workers do gunicorn
# decidem ressuscitar o laço no mesmo segundo.
batimento = BatimentoLaco.objects.first()
if batimento and batimento.fresco and batimento.pid != os.getpid():
self.stdout.write(
'materializar_pdfs: ja existe laco vivo (pid %s, batimento '
'%s) — este processo sai' % (batimento.pid, batimento.visto_em))
return
self._batendo = True
self._tick = tick
self._iniciado_em = timezone.now()
# A versao do codigo em disco no momento da subida. Quando um deploy
# troca o arquivo, o laco RESIDENTE continuaria rodando o codigo velho
# para sempre — entao ele se encerra e deixa a autossupervisao (ou o
# supervisor) ressuscita-lo ja atualizado.
try:
self._versao_codigo = os.path.getmtime(__file__)
except OSError:
self._versao_codigo = None
self.stdout.write(
'materializar_pdfs: laco — varredura completa a cada %ss, fila de '
'prioridade a cada %ss (Ctrl-C para sair)' % (intervalo, tick))
@ -185,6 +211,15 @@ class Command(BaseCommand):
# so um tick de folga.
proxima_varredura = 0.0
while True:
self._bater()
if self._codigo_mudou_no_disco():
self.stdout.write(
'materializar_pdfs: codigo novo no disco — laco sai para '
'renascer atualizado (autossupervisao/supervisor sobem '
'outro)')
logger.info(
'materializar_pdfs: laco encerrado por deploy detectado')
return
try:
if time.monotonic() >= proxima_varredura:
try:
@ -202,6 +237,50 @@ class Command(BaseCommand):
logger.exception('materializar_pdfs: passada falhou: %s', exc)
time.sleep(tick)
# Estado do modo laço (ADR 0015). Fora do laço (passada manual, botão da
# tela, --fila) nada disso é tocado: `_batendo` False faz `_bater` ser um
# no-op e o batimento continua contando só a vida do laço residente.
_batendo = False
_tick = 15
_iniciado_em = None
_versao_codigo = None
_ultimo_batimento_monotonic = 0.0
def _bater(self):
"""Grava o batimento (singleton pk=1), no máximo a cada 10s.
Chamado a cada iteração do laço E a cada matéria da varredura: uma
varredura de uma hora sem batimento pareceria laço morto e faria a
autossupervisão subir um segundo processo.
"""
if not self._batendo:
return
agora = time.monotonic()
if (self._ultimo_batimento_monotonic
and agora - self._ultimo_batimento_monotonic < 10):
return
self._ultimo_batimento_monotonic = agora
try:
BatimentoLaco.objects.update_or_create(pk=1, defaults={
'visto_em': timezone.now(),
'iniciado_em': self._iniciado_em,
'pid': os.getpid(),
'tick_segundos': self._tick})
except Exception as exc: # noqa — batimento é sinal vital, não trabalho:
# falhar em gravá-lo não pode derrubar a materialização em si.
logger.warning('materializar_pdfs: falha ao gravar batimento: %s',
exc)
def _codigo_mudou_no_disco(self):
"""Deploy trocou o arquivo do comando? Então este laço está defasado."""
if self._versao_codigo is None:
return False
try:
return os.path.getmtime(__file__) != self._versao_codigo
except OSError:
# Arquivo sumiu do caminho = deploy em curso: melhor renascer.
return True
def _fechar_orfas(self):
"""Libera o lock de passada cujo processo morreu sem fechar a linha.
@ -323,6 +402,7 @@ class Command(BaseCommand):
gerados = retificados = pulados = falhas = adiados = 0
motivos = {}
for materia, marca in pares:
self._bater() # varredura longa não pode parecer laço morto
try:
resultado, motivo = self._materializar(materia, somente_novos)
except Exception as exc: # noqa — uma matéria não trava as demais (§5.1)

106
sapl/integracao_hub/middleware.py

@ -0,0 +1,106 @@
"""Autossupervisão do laço de materialização (ADR 0015).
O problema que este módulo mata: o SAPL de produção roda fora de docker, e o
laço de materialização dependia de alguém LEMBRAR de subi-lo (nohup, supervisor,
cron todos exigem um ato manual que falhou uma vez, IND 826/2026). O único
processo cuja existência é garantida é o próprio web: o hub bate na API de
integração o tempo inteiro, e cada request é um marca-passo de graça.
Então: o laço grava um batimento no banco a cada tick (`BatimentoLaco`); este
middleware, no máximo uma vez por minuto por worker, confere o batimento e
se ele envelheceu ou nunca existiu ressuscita o laço como processo separado e
desgarrado (mesmo padrão do botão "Rodar agora" do painel: `start_new_session`,
processo sobrevive ao reload do worker).
Corrida entre N workers ressuscitando ao mesmo tempo: inofensiva por
construção o laço que sobe e encontra batimento fresco de outro pid SAI na
hora (guarda de instância única no próprio comando), e o lock de passada
impedia conversão dupla de qualquer forma.
Desligável por `MATERIALIZACAO_AUTOSSUPERVISAO=False` (ambiente onde o laço é
gerido por supervisor/systemd e não se quer o respawn automático).
"""
import logging
import os
import subprocess
import sys
import time
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
logger = logging.getLogger(__name__)
# No máximo uma consulta ao batimento por minuto POR WORKER: o custo por
# request no caminho quente é uma comparação de monotonic.
INTERVALO_VERIFICACAO_SEGUNDOS = 60
_ultima_verificacao = 0.0
def laco_precisa_subir():
"""Batimento ausente ou envelhecido = não há laço vivo."""
from sapl.integracao_hub.models import BatimentoLaco
batimento = BatimentoLaco.objects.first()
return batimento is None or not batimento.fresco
def subir_laco():
"""Sobe o laço como processo separado e desgarrado (padrão do painel).
Intervalo/tick vêm das mesmas variáveis de ambiente do start.sh do
container uma configuração , seja quem for que suba o laço.
"""
from sapl.integracao_hub.painel import _raiz_do_projeto
intervalo = os.environ.get('MATERIALIZACAO_INTERVALO_SEGUNDOS', '300')
tick = os.environ.get('MATERIALIZACAO_TICK_SEGUNDOS', '15')
comando = [
sys.executable, '-m', 'django',
'materializar_pdfs_para_assinatura',
'--intervalo', intervalo,
'--tick', tick,
]
ambiente = os.environ.copy()
ambiente.setdefault('DJANGO_SETTINGS_MODULE', 'sapl.settings')
subprocess.Popen(
comando,
cwd=_raiz_do_projeto(),
env=ambiente,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True)
logger.warning(
'autossupervisao: laco de materializacao sem batimento — novo laco '
'disparado (intervalo=%ss, tick=%ss)', intervalo, tick)
class AutossupervisaoMaterializacao:
def __init__(self, get_response):
# Em teste, spawnar processo residente por efeito colateral de request
# seria vazamento; quem testa as peças chama as funções deste módulo.
if 'pytest' in sys.modules:
raise MiddlewareNotUsed('autossupervisao desligada sob pytest')
if not getattr(settings, 'MATERIALIZACAO_AUTOSSUPERVISAO', True):
raise MiddlewareNotUsed('MATERIALIZACAO_AUTOSSUPERVISAO=False')
self.get_response = get_response
def __call__(self, request):
self._garantir_laco()
return self.get_response(request)
def _garantir_laco(self):
global _ultima_verificacao
agora = time.monotonic()
if _ultima_verificacao and agora - _ultima_verificacao < \
INTERVALO_VERIFICACAO_SEGUNDOS:
return
_ultima_verificacao = agora
try:
if laco_precisa_subir():
subir_laco()
except Exception as exc: # noqa — a autossupervisão NUNCA pode custar
# um request: banco sem migração, tabela ausente, falha de spawn —
# tudo vira log e o SAPL segue servindo.
logger.warning('autossupervisao: verificacao falhou (%s) — '
'seguira tentando a cada %ss', exc,
INTERVALO_VERIFICACAO_SEGUNDOS)

25
sapl/integracao_hub/migrations/0007_batimento_laco.py

@ -0,0 +1,25 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('integracao_hub', '0006_fila_prioridade_materializacao'),
]
operations = [
migrations.CreateModel(
name='BatimentoLaco',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('visto_em', models.DateTimeField(verbose_name='Último batimento')),
('iniciado_em', models.DateTimeField(verbose_name='Laço iniciado em')),
('pid', models.IntegerField(verbose_name='PID do processo do laço')),
('tick_segundos', models.IntegerField(default=15, help_text='Gravado pelo próprio laço: é a referência para decidir se o batimento envelheceu.', verbose_name='Tick configurado (s)')),
],
options={
'verbose_name': 'Batimento do Laço de Materialização',
'verbose_name_plural': 'Batimentos do Laço de Materialização',
},
),
]

44
sapl/integracao_hub/models.py

@ -363,3 +363,47 @@ class MateriaParaMaterializar(models.Model):
def __str__(self):
return 'Matéria %s na fila desde %s' % (self.materia_id,
self.marcada_em)
class BatimentoLaco(models.Model):
"""Batimento do laço de materialização — singleton (pk=1), ADR 0015.
O laço grava aqui a cada tick (e durante varreduras longas, a cada poucos
segundos). É o que permite a autossupervisão: o middleware do processo web
o único processo cuja existência é garantida, porque o hub bate na API o
tempo todo este batimento e, se ele envelhecer, ressuscita o laço.
Ninguém precisa lembrar de subir nada: nem supervisor, nem cron, nem Léo.
Também é o guarda de instância única: um laço que sobe e encontra
batimento fresco de OUTRO processo sai imediatamente o lock de passada
impedia conversão dupla; este impede dois laços vivos disputando.
"""
visto_em = models.DateTimeField(
verbose_name=_('Último batimento'))
iniciado_em = models.DateTimeField(
verbose_name=_('Laço iniciado em'))
pid = models.IntegerField(
verbose_name=_('PID do processo do laço'))
tick_segundos = models.IntegerField(
default=15,
verbose_name=_('Tick configurado (s)'),
help_text=_('Gravado pelo próprio laço: é a referência para decidir '
'se o batimento envelheceu.'))
class Meta:
verbose_name = _('Batimento do Laço de Materialização')
verbose_name_plural = _('Batimentos do Laço de Materialização')
def __str__(self):
return 'Laço pid %s, batimento %s' % (self.pid, self.visto_em)
@property
def fresco(self):
"""Vivo = bateu há menos de 3 ticks (piso de 60s para folga de carga)."""
from django.utils import timezone
janela = max(3 * (self.tick_segundos or 15), 60)
return (timezone.now() - self.visto_em).total_seconds() < janela

9
sapl/integracao_hub/painel.py

@ -41,8 +41,9 @@ from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import TemplateView, View
from .models import (DocumentoParaAssinatura, MateriaComFalhaMaterializacao,
MateriaParaMaterializar, PassadaMaterializacao)
from .models import (BatimentoLaco, DocumentoParaAssinatura,
MateriaComFalhaMaterializacao, MateriaParaMaterializar,
PassadaMaterializacao)
logger = logging.getLogger(__name__)
@ -108,6 +109,10 @@ class PainelMaterializacaoView(PermissionRequiredMixin, TemplateView):
# Fila de prioridade (ADR 0014): matérias marcadas pelo evento
# aguardando o tick. Cresce e não esvazia = o laço não está de pé.
'na_fila': MateriaParaMaterializar.objects.count(),
# Batimento do laço (ADR 0015): a resposta de "está rodando?" sem
# shell. Morto aqui + fila crescendo = autossupervisão também
# falhou (aí sim é caso de shell).
'batimento': BatimentoLaco.objects.first(),
# A configuração que decide o sucesso da conversão fica na tela
# porque foi exatamente ela a causa do incidente de 25/08 — e lê-la
# exigia shell.

22
sapl/integracao_hub/templates/integracao_hub/painel_materializacao.html

@ -43,6 +43,28 @@
</div>
{% endif %}
<p>
{% if batimento %}
{% if batimento.fresco %}
<span class="text-success">
<strong>Laço automático vivo</strong> (pid {{ batimento.pid }},
último batimento {{ batimento.visto_em }}).
</span>
{% else %}
<span class="text-danger">
<strong>Laço automático SEM batimento</strong> desde
{{ batimento.visto_em }} — a autossupervisão vai ressuscitá-lo no
próximo acesso ao sistema; se esta mensagem persistir, o respawn
está falhando (ver sapl.log).
</span>
{% endif %}
{% else %}
<span class="text-warning">
<strong>Laço automático nunca bateu</strong> — sobe sozinho no
primeiro acesso após o deploy desta versão.
</span>
{% endif %}
</p>
<p>
<strong>{{ alvos_materializados }}</strong> documento(s) já materializado(s)
e disponível(is) para assinatura.

161
sapl/integracao_hub/tests/test_autossupervisao.py

@ -0,0 +1,161 @@
"""Autossupervisão do laço de materialização (ADR 0015).
O contrato: o laço bate no banco a cada tick; o processo web ressuscita o laço
quando o batimento envelhece; dois laços nunca residem juntos; e um deploy faz
o laço se encerrar para renascer com o código novo.
"""
import os
import pytest
from django.core.management import call_command
from django.utils import timezone
from sapl.integracao_hub.middleware import laco_precisa_subir, subir_laco
from sapl.integracao_hub.models import BatimentoLaco, PassadaMaterializacao
CMD = ('sapl.integracao_hub.management.commands'
'.materializar_pdfs_para_assinatura')
@pytest.fixture(autouse=True)
def base_url_configurada(settings):
settings.SAPL_INTERNAL_URL = 'http://sapl-interno:8000'
settings.SITE_URL = ''
def batimento(segundos_atras=0, pid=None):
agora = timezone.now()
return BatimentoLaco.objects.create(
pk=1,
visto_em=agora - timezone.timedelta(seconds=segundos_atras),
iniciado_em=agora,
pid=pid if pid is not None else os.getpid() + 1,
tick_segundos=15)
# ---------------------------------------------------------------------------
# O laço bate — inclusive é este batimento que o painel exibe
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=False)
def test_modo_laco_grava_batimento(db, monkeypatch):
def sleep_que_interrompe(segundos):
raise KeyboardInterrupt
monkeypatch.setattr(CMD + '.time.sleep', sleep_que_interrompe)
with pytest.raises(KeyboardInterrupt):
call_command('materializar_pdfs_para_assinatura', intervalo=30)
vivo = BatimentoLaco.objects.get(pk=1)
assert vivo.pid == os.getpid()
assert vivo.fresco
assert vivo.tick_segundos == 15
@pytest.mark.django_db(transaction=False)
def test_passada_avulsa_nao_bate(db):
"""O batimento mede a vida do LAÇO; passada manual não é laço."""
call_command('materializar_pdfs_para_assinatura')
assert not BatimentoLaco.objects.exists()
# ---------------------------------------------------------------------------
# Instância única: dois laços nunca residem juntos
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=False)
def test_segundo_laco_sai_quando_ha_batimento_fresco_de_outro_pid(db):
batimento(segundos_atras=5)
# Sem monkeypatch de sleep: se o guarda falhar, o laço entraria em loop
# infinito — o retorno imediato É a asserção.
call_command('materializar_pdfs_para_assinatura', intervalo=30)
assert not PassadaMaterializacao.objects.exists()
@pytest.mark.django_db(transaction=False)
def test_laco_assume_quando_batimento_esta_velho(db, monkeypatch):
batimento(segundos_atras=600)
def sleep_que_interrompe(segundos):
raise KeyboardInterrupt
monkeypatch.setattr(CMD + '.time.sleep', sleep_que_interrompe)
with pytest.raises(KeyboardInterrupt):
call_command('materializar_pdfs_para_assinatura', intervalo=30)
assert BatimentoLaco.objects.get(pk=1).pid == os.getpid()
# ---------------------------------------------------------------------------
# Deploy: laço defasado se encerra para renascer com o código novo
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=False)
def test_laco_sai_quando_o_codigo_muda_no_disco(db, monkeypatch):
getmtime_real = os.path.getmtime
contagem = {'n': 0}
def getmtime_que_avanca(caminho):
# Só o arquivo do comando "muda de versão"; o resto do mundo segue
# normal — getmtime é global e outros códigos o usam no meio do teste.
if str(caminho).endswith('materializar_pdfs_para_assinatura.py'):
contagem['n'] += 1
return float(contagem['n'])
return getmtime_real(caminho)
monkeypatch.setattr(CMD + '.os.path.getmtime', getmtime_que_avanca)
# Retorna sozinho (sem KeyboardInterrupt): a saída limpa É a asserção.
call_command('materializar_pdfs_para_assinatura', intervalo=30)
# ---------------------------------------------------------------------------
# O lado do processo web: decidir e ressuscitar
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=False)
def test_precisa_subir_sem_batimento_ou_com_batimento_velho(db):
assert laco_precisa_subir() # nunca bateu
b = batimento(segundos_atras=600)
assert laco_precisa_subir() # bateu ha 10min com tick de 15s
b.visto_em = timezone.now()
b.save()
assert not laco_precisa_subir() # fresco
@pytest.mark.django_db(transaction=False)
def test_subir_laco_dispara_processo_desgarrado(db, monkeypatch):
chamadas = []
def popen_falso(comando, **kwargs):
chamadas.append((comando, kwargs))
monkeypatch.setattr(
'sapl.integracao_hub.middleware.subprocess.Popen', popen_falso)
monkeypatch.setenv('MATERIALIZACAO_INTERVALO_SEGUNDOS', '120')
monkeypatch.setenv('MATERIALIZACAO_TICK_SEGUNDOS', '5')
subir_laco()
comando, kwargs = chamadas[0]
assert 'materializar_pdfs_para_assinatura' in comando
assert comando[comando.index('--intervalo') + 1] == '120'
assert comando[comando.index('--tick') + 1] == '5'
# Desgarrado do worker: sobrevive ao reload do gunicorn.
assert kwargs['start_new_session'] is True
def test_middleware_nao_liga_sob_pytest():
"""Spawnar laço residente por efeito de request de teste seria vazamento."""
from django.core.exceptions import MiddlewareNotUsed
from sapl.integracao_hub.middleware import AutossupervisaoMaterializacao
with pytest.raises(MiddlewareNotUsed):
AutossupervisaoMaterializacao(lambda request: None)

10
sapl/settings.py

@ -156,6 +156,10 @@ MIDDLEWARE = [
'waffle.middleware.WaffleMiddleware',
'sapl.middleware.CheckWeakPasswordMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware',
# Autossupervisão da materialização (ADR 0015): cada request confere (no
# máximo 1x/min) se o laço do PDF-alvo está vivo e o ressuscita se não —
# o SAPL de produção roda fora de docker e nada mais garante esse processo.
'sapl.integracao_hub.middleware.AutossupervisaoMaterializacao',
]
if DEBUG and 'runserver' in sys.argv:
INSTALLED_APPS += ('debug_toolbar',)
@ -165,6 +169,12 @@ if DEBUG and 'runserver' in sys.argv:
SITE_URL = config('SITE_URL', cast=str, default='')
# Desligue APENAS onde o laço de materialização é gerido por fora (supervisor/
# systemd) e o respawn automático atrapalharia. O padrão ligado é o que
# garante a materialização sem depender de memória de operador (ADR 0015).
MATERIALIZACAO_AUTOSSUPERVISAO = config(
'MATERIALIZACAO_AUTOSSUPERVISAO', cast=bool, default=True)
REST_FRAMEWORK = {
"UNICODE_JSON": False,
"DEFAULT_PARSER_CLASSES": (

5
scripts/atualizar_producao.sh

@ -7,3 +7,8 @@ pip install -r requirements/dev-requirements.txt
./manage.py collectstatic --noinput
deactivate
sudo supervisorctl restart sapl
# Materializacao do PDF-alvo (ADR 0014): reinicia junto do deploy para pegar
# codigo novo. Se o programa ainda nao existe no supervisor, instalar com
# scripts/supervisor-materializacao.conf (instrucoes no cabecalho do arquivo).
sudo supervisorctl restart sapl-materializacao \
|| echo "AVISO: programa sapl-materializacao nao instalado no supervisor — sem ele, materia protocolada NAO vira pendencia no app (ver scripts/supervisor-materializacao.conf)"

35
scripts/supervisor-materializacao.conf

@ -0,0 +1,35 @@
; Laco da materializacao do PDF-alvo da assinatura (ADR 0014).
;
; Sem este programa rodando, materia protocolada NUNCA vira pendencia de
; assinatura no app — so por clique manual no painel, em silencio. O SAPL de
; producao roda fora de docker, entao o laco nao sobe sozinho com o servico:
; e o supervisor (o mesmo que mantem o gunicorn) quem garante boot + restart.
;
; Instalacao (uma vez):
; sudo cp scripts/supervisor-materializacao.conf /etc/supervisor/conf.d/sapl-materializacao.conf
; -> ajustar command/directory/user espelhando o [program:sapl] ja existente
; em /etc/supervisor/conf.d/ (mesmo venv, mesmo diretorio, mesmo usuario)
; sudo supervisorctl reread && sudo supervisorctl update
; sudo supervisorctl status sapl-materializacao
;
; Acompanhamento: o painel /sistema/integracao/materializacao/ — fila de
; prioridade crescendo sem esvaziar = este programa caiu.
[program:sapl-materializacao]
; AJUSTAR os tres caminhos abaixo para o layout da VPS (copiar do [program:sapl])
command=/var/interlegis/.virtualenvs/sapl/bin/python manage.py materializar_pdfs_para_assinatura --intervalo 300 --tick 15
directory=/var/interlegis/sapl
user=sapl
autostart=true
autorestart=true
startsecs=10
; O SIGTERM derruba o laco no meio de uma passada sem drama: a linha aberta em
; PassadaMaterializacao fica orfa e a propria rotina a fecha e destrava o lock
; na subida seguinte (_fechar_orfas).
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/log/sapl-materializacao.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=3
Loading…
Cancel
Save