Skip to content

Fix too many open files issue#1443

Open
rondinelisaad wants to merge 1 commit into
mainfrom
codex/too-many-openfile-issue
Open

Fix too many open files issue#1443
rondinelisaad wants to merge 1 commit into
mainfrom
codex/too-many-openfile-issue

Conversation

@rondinelisaad

Copy link
Copy Markdown
Member

O que esse PR faz?

Este PR corrige um problema em que falhas na coleta de métricas de memória pelo sistema de profiling resultavam em erro HTTP 500 para o usuário final.

A leitura de memória realizada por meio da biblioteca psutil foi centralizada em funções auxiliares seguras em core/utils/profiling_tools.py. Quando ocorre uma exceção como:
OSError: [Errno 24] Too many open files: '/proc/16/statm'
ERROR 2026-06-15 22:35:00,539 log 16 139626005938016 Internal Server Error: /api/v2/pid/pid_provider/
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/app/core/utils/profiling_tools.py", line 589, in call
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 461, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 459, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/init.py", line 1139, in memory_info
File "/usr/local/lib/python3.11/site-packages/psutil/_pslinux.py", line 1638, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/_pslinux.py", line 1921, in memory_info
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 764, in open_binary
OSError: [Errno 24] Too many open files: '/proc/16/statm'

durante a leitura de informações do processo (/proc//statm), o sistema passa a tratar a métrica de memória como indisponível (unavailable) e permite que a requisição continue normalmente.

Além disso:

  • Middleware e decoradores foram ajustados para tratar adequadamente situações em que a métrica de memória não está disponível;
  • Comparações entre valores de memória e limites configurados deixam de ser executadas quando não há métrica válida;
  • O cabeçalho HTTP X-Memory-Used passa a ser enviado apenas quando a informação de memória foi obtida com sucesso.

O objetivo é evitar que uma falha de observabilidade/profiling impacte a disponibilidade da aplicação.

Onde a revisão poderia começar?

Recomenda-se iniciar a revisão pelo arquivo:
core/utils/profiling_tools.py

Principais alterações:

  • Criação dos helpers seguros para obtenção de métricas de memória;
  • Tratamento de exceções relacionadas ao psutil;
  • Ajustes nos pontos que utilizam a métrica de memória.

Como este poderia ser testado manualmente?

Cenário normal

  1. Subir a aplicação normalmente;
  2. Acessar páginas da aplicação;
  3. Confirmar que as respostas continuam sendo retornadas normalmente;
  4. Verificar que o cabeçalho X-Memory-Used continua sendo enviado quando a coleta de memória é bem-sucedida.

Cenário de falha na coleta de memória

  1. Simular uma falha na chamada ao psutil.Process(...).memory_info();
  2. Realizar uma requisição para a aplicação;
  3. Confirmar que a resposta continua sendo retornada normalmente (sem HTTP 500);
  4. Confirmar que o cabeçalho X-Memory-Used não é enviado quando a métrica não está disponível;
  5. Verificar nos logs que a falha foi tratada adequadamente pelo profiling.

Algum cenário de contexto que queira dar?

Foi identificado um incidente em produção onde a aplicação retornava HTTP 500 devido a uma exceção originada no mecanismo de profiling:
OSError: [Errno 24] Too many open files: '/proc/16/statm'
ERROR 2026-06-15 22:35:00,539 log 16 139626005938016 Internal Server Error: /api/v2/pid/pid_provider/
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/app/core/utils/profiling_tools.py", line 589, in call
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 461, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 459, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/init.py", line 1139, in memory_info
File "/usr/local/lib/python3.11/site-packages/psutil/_pslinux.py", line 1638, in wrapper
File "/usr/local/lib/python3.11/site-packages/psutil/_pslinux.py", line 1921, in memory_info
File "/usr/local/lib/python3.11/site-packages/psutil/_common.py", line 764, in open_binary
OSError: [Errno 24] Too many open files: '/proc/16/statm'

A exceção ocorria durante a leitura de métricas de memória via psutil, fazendo com que um componente de observabilidade interrompesse o processamento normal da requisição.

Este PR atua apenas como mecanismo de proteção (graceful degradation), impedindo que falhas de monitoramento provoquem indisponibilidade da aplicação.

Importante destacar que a causa raiz do erro (Too many open files) não é tratada neste PR e deve continuar sendo investigada. Possíveis causas incluem:

  • Vazamento de descritores de arquivos;
  • Excesso de conexões abertas;
  • Limites de ulimit insuficientes;
  • Limitações configuradas no container ou ambiente de execução.

Screenshots

Não se aplica.

Quais são tickets relevantes?

Não há ticket associado no momento.

Referências

  • Documentação do psutil sobre Process.memory_info()
  • Tratamento de exceções para degradação graciosa de componentes de observabilidade
  • Incidente de produção relacionado ao erro OSError: [Errno 24] Too many open files

Obs.: o comportamento mudou de “fail closed” para “fail open”: se o profiling falhar, a aplicação continua atendendo a requisição. Isso deixa explícita a decisão arquitetural tomada e facilita a aprovação pelos revisores.

@gitnnolabs

Copy link
Copy Markdown
Collaborator

What This PR Does

This PR implements graceful degradation for the profiling system to prevent monitoring failures from causing application outages. Specifically, it addresses an incident where OSError: [Errno 24] Too many open files exceptions from psutil during memory metric collection were causing HTTP 500 errors.

Key behavior change: From "fail closed" (profiling errors crash the request) to "fail open" (profiling errors are silently handled, memory metrics marked unavailable).

Business Impact

  • Problem: Production incident where profiling system failures resulted in HTTP 500s
  • Solution: Centralize memory metric collection with exception handling, gracefully degrade to "unavailable" state
  • Outcome: Application remains available even when profiling can't collect metrics

Core Changes

The PR introduces four new helper functions that safely wrap memory metric collection:

def _get_memory_usage_mb(process=None):
    """Returns RSS in MB or None if psutil fails"""
    try:
        if process is None:
            process = psutil.Process()
        return process.memory_info().rss / 1024 / 1024
    except (OSError, psutil.Error) as exc:
        profiling_logger.debug("Skipping profiling memory sample: %s", exc)
        return None

def _get_memory_delta_mb(process, start_memory):
    """Safely calculates memory delta, returns None if either value unavailable"""
    if start_memory is None:
        return None
    end_memory = _get_memory_usage_mb(process)
    if end_memory is None:
        return None
    return end_memory - start_memory

def _format_memory_delta(memory_used):
    """Formats memory delta or returns 'unavailable' string"""
    if memory_used is None:
        return "unavailable"
    return f"+{memory_used:.1f}MB"

def _is_high_memory(memory_used):
    """Safely checks if memory exceeded threshold"""
    return memory_used is not None and memory_used > PROFILING_LOG_HIGH_MEMORY

Why this matters:

  • Catches OSError (file descriptor exhaustion) and psutil.Error
  • Returns None instead of raising, allowing callers to handle gracefully
  • Single point of truth for memory collection logic

Other Changes

All profiling decorators and middleware are updated to use these helpers:

  1. profile_endpoint decorator - HTTP request profiling middleware

    • ✅ Uses _get_memory_usage_mb() for initial memory
    • ✅ Uses _get_memory_delta_mb() to compute delta
    • ✅ Only sends X-Memory-Used header when metric is available (if memory_used is not None)
  2. profile_classmethod, profile_method, profile_function, profile_staticmethod - All decorators updated

    • ✅ Replaced inline process.memory_info() calls with helpers
    • ✅ Updated memory comparisons to use _is_high_memory()
    • ✅ Updated logging to use _format_memory_delta()
  3. LightweightProfilingMiddleware - Global middleware

    • ✅ Uses helpers for safe memory collection
    • ✅ Logs "unavailable" instead of crashing on missing metrics

Consistency: All 7 locations that read memory metrics now follow the same safe pattern.


Merge Readiness and Risk Assessment

✅ Strengths

  1. Defensive Design: Centralized exception handling prevents profiling system failures from breaking the app
  2. Backward Compatible: No breaking changes to APIs or behaviors (only logging and response headers change)
  3. Well-Documented: Clear comments explain the failure mode and design decision ("fail open")
  4. Comprehensive: All profiling decorators and middleware updated consistently
  5. Minimal Scope: Single file, focused change with clear intent

⚠️ Considerations

  1. Root Cause Not Fixed: PR addresses symptoms (graceful degradation) not root cause (file descriptor exhaustion). Per the PR description, the underlying "too many open files" issue should continue to be investigated separately.

  2. Logging Clarity: When memory metrics fail, debug logs show profiling_logger.debug() but regular logs show "unavailable". This is acceptable since profiling failures are not critical, but ensures you won't see metrics gaps in normal logs.

  3. Header Behavior Change: X-Memory-Used header now omitted when metrics unavailable (was previously always sent). Any downstream systems relying on this header should handle its absence. This is correct behavior but worth noting.

  4. No Tests Visible: PR shows code changes but no visible unit/integration tests. Recommend:

    • Unit test for each helper function with mocked psutil failures
    • Integration test verifying requests succeed even when psutil.Process().memory_info() raises OSError

Risk Assessment: 🟢 Low

  • Isolated change with clear scope
  • Adds safety without removing functionality
  • Follows defensive programming principles
  • Only impacts profiling observability, not core app logic

Possible Improvements

  1. Add Unit Tests

    # Test that _get_memory_usage_mb handles OSError gracefully
    @patch('psutil.Process.memory_info', side_effect=OSError(24, 'Too many open files'))
    def test_get_memory_usage_mb_handles_oserror(mock_memory):
        result = _get_memory_usage_mb()
        assert result is None
  2. Consider Metrics Fallback: If psutil is unreliable, consider falling back to /proc/self/status parsing (for Linux) or os.getpid() + manual parsing as a secondary method.

  3. Metrics Reporting: Consider adding a metric (to APM/monitoring) when profiling sampling fails, so the ops team can track how often this degradation is triggered.

  4. Inline Comments: Add a comment above each decorator update explaining why None checks are needed, to future-proof the code.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants