Skip to content

Timeout exception when removing a billable member via the API/UI

Summary

When utilizing the remove billable member API call, a Rack::Timeout::RequestTimeoutException can occur if the user has membership within a large number of projects.

This same exception occurs when attempting to remove the user via the UI.

Steps to reproduce

Perform the relevant request on a user with membership in a large number of projects. This occurred in this ticket (internal use) for a user who had 1,000+ project memberships.

What is the current bug behavior?

Attempting to remove a billable member via this call can result in a Rack::Timeout::RequestTimeoutException with the user's memberships remaining unaffected.

What is the expected correct behavior?

The request is appropriately processed and the target user memberships are removed.

Relevant logs and/or screenshots

Logs can be seen (for now) in Kibana (internal use). Otherwise, the exception can be seen below.

Exception logs
 "json": {
      "exception.class": "Rack::Timeout::RequestTimeoutException",
      "exception.message": "Request ran for longer than 60000ms ",
      "method": "DELETE",
      "route": "/api/:version/groups/:id/billable_members/:user_id",
      "meta.caller_id": "DELETE /api/:version/groups/:id/billable_members/:user_id",
      "exception.backtrace": [
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `block (2 levels) in exec_no_cache'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/share_lock.rb:187:in `yield_shares'",
        "activesupport (7.0.8.1) lib/active_support/dependencies/interlock.rb:41:in `permit_concurrent_loads'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql_adapter.rb:767:in `block in exec_no_cache'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract_adapter.rb:752:in `block in log'",
        "activesupport (7.0.8.1) lib/active_support/notifications/instrumenter.rb:24:in `instrument'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract_adapter.rb:743:in `log'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql_adapter.rb:766:in `exec_no_cache'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql_adapter.rb:745:in `execute_and_clear'",
        "marginalia (1.11.1) lib/marginalia.rb:91:in `execute_and_clear_with_marginalia'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/postgresql/database_statements.rb:54:in `exec_query'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/database_statements.rb:560:in `select'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/database_statements.rb:66:in `select_all'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/query_cache.rb:107:in `block in select_all'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/query_cache.rb:137:in `block in cache_sql'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'",
        "activesupport (7.0.8.1) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/query_cache.rb:128:in `cache_sql'",
        "activerecord (7.0.8.1) lib/active_record/connection_adapters/abstract/query_cache.rb:107:in `select_all'",
        "lib/gitlab/database/load_balancing/connection_proxy.rb:107:in `public_send'",
        "lib/gitlab/database/load_balancing/connection_proxy.rb:107:in `block in read_using_load_balancer'",
        "lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'",
        "lib/gitlab/database/load_balancing/load_balancer.rb:235:in `retry_with_backoff'",
        "lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'",
        "lib/gitlab/database/load_balancing/connection_proxy.rb:106:in `read_using_load_balancer'",
        "lib/gitlab/database/load_balancing/connection_proxy.rb:48:in `select_all'",
        "activerecord (7.0.8.1) lib/active_record/relation/calculations.rb:250:in `block in pluck'",
        "activerecord (7.0.8.1) lib/active_record/relation.rb:962:in `skip_query_cache_if_necessary'",
        "activerecord (7.0.8.1) lib/active_record/relation/calculations.rb:246:in `pluck'",
        "lockbox (1.3.0) lib/lockbox/calculations.rb:4:in `pluck'",
        "ee/app/finders/namespaces/billed_users_finder.rb:34:in `block in calculate_user_ids'",
        "lib/gitlab/database.rb:217:in `allow_cross_joins_across_databases'",
        "ee/app/finders/namespaces/billed_users_finder.rb:32:in `calculate_user_ids'",
        "ee/app/finders/namespaces/billed_users_finder.rb:13:in `block in execute'",
        "ee/app/finders/namespaces/billed_users_finder.rb:12:in `each_key'",
        "ee/app/finders/namespaces/billed_users_finder.rb:12:in `execute'",
        "ee/app/models/ee/group.rb:1070:in `block in billed_user_ids_including_guests'",
        "gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb:34:in `strong_memoize'",
        "ee/app/models/ee/group.rb:1069:in `billed_user_ids_including_guests'",
        "ee/app/models/ee/group.rb:597:in `billed_user_ids'",
        "ee/app/models/ee/user.rb:464:in `using_gitlab_com_seat?'",
        "ee/app/models/ee/member.rb:154:in `is_using_seat'",
        "ee/app/services/ee/members/destroy_service.rb:22:in `after_execute'",
        "app/services/members/destroy_service.rb:101:in `delete_member_associations'",
        "app/services/members/destroy_service.rb:85:in `destroy_data_related_to_member'",
        "ee/app/services/ee/members/destroy_service.rb:112:in `destroy_data_related_to_member'",
        "app/services/members/destroy_service.rb:29:in `execute'",
        "ee/app/services/billable_members/destroy_service.rb:45:in `block in remove_user_from_resources'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:71:in `each'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:71:in `block in find_each'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:138:in `block in find_in_batches'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:245:in `block in in_batches'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:229:in `loop'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:229:in `in_batches'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:137:in `find_in_batches'",
        "activerecord (7.0.8.1) lib/active_record/relation/batches.rb:70:in `find_each'",
        "ee/app/services/billable_members/destroy_service.rb:43:in `remove_user_from_resources'",
        "ee/app/services/billable_members/destroy_service.rb:27:in `execute'",
        "ee/lib/ee/api/members.rb:227:in `block (3 levels) in <module:Members>'",
        "grape (2.0.0) lib/grape/endpoint.rb:58:in `call'",
        "grape (2.0.0) lib/grape/endpoint.rb:58:in `block (2 levels) in generate_api_method'",
        "activesupport (7.0.8.1) lib/active_support/notifications.rb:208:in `instrument'",
        "grape (2.0.0) lib/grape/endpoint.rb:57:in `block in generate_api_method'",
        "grape (2.0.0) lib/grape/endpoint.rb:328:in `execute'",
        "grape (2.0.0) lib/grape/endpoint.rb:260:in `block in run'",
        "activesupport (7.0.8.1) lib/active_support/notifications.rb:208:in `instrument'",
        "grape (2.0.0) lib/grape/endpoint.rb:240:in `run'",
        "grape (2.0.0) lib/grape/endpoint.rb:316:in `block in build_stack'",
        "grape (2.0.0) lib/grape/middleware/base.rb:36:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "grape (2.0.0) lib/grape/middleware/base.rb:36:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "ee/lib/gitlab/middleware/ip_restrictor.rb:14:in `block in call'",
        "ee/lib/gitlab/ip_address_state.rb:10:in `with'",
        "ee/lib/gitlab/middleware/ip_restrictor.rb:13:in `call'",
        "grape (2.0.0) lib/grape/middleware/base.rb:36:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "lib/api/api_guard.rb:219:in `call'",
        "grape (2.0.0) lib/grape/middleware/base.rb:36:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "rack-oauth2 (1.21.3) lib/rack/oauth2/server/resource.rb:20:in `_call'",
        "rack-oauth2 (1.21.3) lib/rack/oauth2/server/resource/bearer.rb:8:in `_call'",
        "rack-oauth2 (1.21.3) lib/rack/oauth2/server/abstract/handler.rb:17:in `call'",
        "grape (2.0.0) lib/grape/middleware/error.rb:39:in `block in call!'",
        "grape (2.0.0) lib/grape/middleware/error.rb:38:in `catch'",
        "grape (2.0.0) lib/grape/middleware/error.rb:38:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "grape_logging (1.8.4) lib/grape_logging/middleware/request_logger.rb:60:in `block in call!'",
        "grape_logging (1.8.4) lib/grape_logging/middleware/request_logger.rb:58:in `catch'",
        "grape_logging (1.8.4) lib/grape_logging/middleware/request_logger.rb:58:in `call!'",
        "grape (2.0.0) lib/grape/middleware/base.rb:29:in `call'",
        "rack (2.2.8.1) lib/rack/head.rb:12:in `call'",
        "grape (2.0.0) lib/grape/endpoint.rb:224:in `call!'",
        "grape (2.0.0) lib/grape/endpoint.rb:218:in `call'",
        "grape (2.0.0) lib/grape/router/route.rb:58:in `exec'",
        "grape (2.0.0) lib/grape/router.rb:120:in `process_route'",
        "grape (2.0.0) lib/grape/router.rb:74:in `block in identity'",
        "grape (2.0.0) lib/grape/router.rb:94:in `transaction'",
        "grape (2.0.0) lib/grape/router.rb:72:in `identity'",
        "grape (2.0.0) lib/grape/router.rb:56:in `block in call'",
        "grape (2.0.0) lib/grape/router.rb:136:in `with_optimization'",
        "grape (2.0.0) lib/grape/router.rb:55:in `call'",
        "grape (2.0.0) lib/grape/api/instance.rb:165:in `call'",
        "grape (2.0.0) lib/grape/api/instance.rb:70:in `call!'",
        "grape (2.0.0) lib/grape/api/instance.rb:65:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/routing/mapper.rb:19:in `block in <class:Constraints>'",
        "actionpack (7.0.8.1) lib/action_dispatch/routing/mapper.rb:48:in `serve'",
        "actionpack (7.0.8.1) lib/action_dispatch/journey/router.rb:50:in `block in serve'",
        "actionpack (7.0.8.1) lib/action_dispatch/journey/router.rb:32:in `each'",
        "actionpack (7.0.8.1) lib/action_dispatch/journey/router.rb:32:in `serve'",
        "actionpack (7.0.8.1) lib/action_dispatch/routing/route_set.rb:852:in `call'",
        "gitlab-experiment (0.9.1) lib/gitlab/experiment/middleware.rb:19:in `call'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:202:in `call!'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:169:in `call'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:470:in `call_app!'",
        "ee/lib/omni_auth/strategies/group_saml.rb:41:in `other_phase'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:195:in `call!'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:169:in `call'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:202:in `call!'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:169:in `call'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:202:in `call!'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:169:in `call'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:202:in `call!'",
        "omniauth (2.1.0) lib/omniauth/strategy.rb:169:in `call'",
        "flipper (0.26.2) lib/flipper/middleware/memoizer.rb:72:in `memoized_call'",
        "flipper (0.26.2) lib/flipper/middleware/memoizer.rb:37:in `call'",
        "lib/gitlab/metrics/elasticsearch_rack_middleware.rb:16:in `call'",
        "lib/gitlab/middleware/sidekiq_shard_awareness_validation.rb:20:in `block in call'",
        "lib/gitlab/sidekiq_sharding/validator.rb:42:in `enabled'",
        "lib/gitlab/middleware/sidekiq_shard_awareness_validation.rb:20:in `call'",
        "lib/gitlab/middleware/memory_report.rb:13:in `call'",
        "lib/gitlab/middleware/speedscope.rb:13:in `call'",
        "lib/gitlab/database/load_balancing/rack_middleware.rb:23:in `call'",
        "lib/gitlab/middleware/rails_queue_duration.rb:33:in `call'",
        "lib/gitlab/etag_caching/middleware.rb:21:in `call'",
        "lib/gitlab/metrics/rack_middleware.rb:16:in `block in call'",
        "lib/gitlab/metrics/web_transaction.rb:46:in `run'",
        "lib/gitlab/metrics/rack_middleware.rb:16:in `call'",
        "lib/gitlab/middleware/go.rb:20:in `call'",
        "lib/gitlab/middleware/query_analyzer.rb:11:in `block in call'",
        "lib/gitlab/database/query_analyzer.rb:40:in `within'",
        "lib/gitlab/middleware/query_analyzer.rb:11:in `call'",
        "lib/gitlab/middleware/organizations/current.rb:20:in `call'",
        "batch-loader (2.0.5) lib/batch_loader/middleware.rb:11:in `call'",
        "rack-attack (6.7.0) lib/rack/attack.rb:103:in `call'",
        "apollo_upload_server (2.1.6) lib/apollo_upload_server/middleware.rb:19:in `call'",
        "lib/gitlab/middleware/multipart.rb:173:in `call'",
        "rack-attack (6.7.0) lib/rack/attack.rb:127:in `call'",
        "warden (1.2.9) lib/warden/manager.rb:36:in `block in call'",
        "warden (1.2.9) lib/warden/manager.rb:34:in `catch'",
        "warden (1.2.9) lib/warden/manager.rb:34:in `call'",
        "rack-cors (2.0.1) lib/rack/cors.rb:102:in `call'",
        "rack (2.2.8.1) lib/rack/tempfile_reaper.rb:15:in `call'",
        "rack (2.2.8.1) lib/rack/etag.rb:27:in `call'",
        "rack (2.2.8.1) lib/rack/conditional_get.rb:40:in `call'",
        "rack (2.2.8.1) lib/rack/head.rb:12:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/http/permissions_policy.rb:38:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/http/content_security_policy.rb:36:in `call'",
        "lib/gitlab/middleware/read_only/controller.rb:50:in `call'",
        "lib/gitlab/middleware/read_only.rb:18:in `call'",
        "lib/gitlab/middleware/unauthenticated_session_expiry.rb:18:in `call'",
        "rack (2.2.8.1) lib/rack/session/abstract/id.rb:266:in `context'",
        "rack (2.2.8.1) lib/rack/session/abstract/id.rb:260:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/cookies.rb:704:in `call'",
        "lib/gitlab/middleware/same_site_cookies.rb:27:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'",
        "activesupport (7.0.8.1) lib/active_support/callbacks.rb:99:in `run_callbacks'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/callbacks.rb:26:in `call'",
        "sentry-rails (5.17.3) lib/sentry/rails/rescued_exception_interceptor.rb:12:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/debug_exceptions.rb:28:in `call'",
        "lib/gitlab/middleware/path_traversal_check.rb:21:in `call'",
        "lib/gitlab/middleware/handle_malformed_strings.rb:21:in `call'",
        "sentry-ruby (5.17.3) lib/sentry/rack/capture_exceptions.rb:29:in `block (2 levels) in call'",
        "sentry-ruby (5.17.3) lib/sentry/hub.rb:251:in `with_session_tracking'",
        "sentry-ruby (5.17.3) lib/sentry-ruby.rb:403:in `with_session_tracking'",
        "sentry-ruby (5.17.3) lib/sentry/rack/capture_exceptions.rb:20:in `block in call'",
        "sentry-ruby (5.17.3) lib/sentry/hub.rb:59:in `with_scope'",
        "sentry-ruby (5.17.3) lib/sentry-ruby.rb:383:in `with_scope'",
        "sentry-ruby (5.17.3) lib/sentry/rack/capture_exceptions.rb:19:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/show_exceptions.rb:29:in `call'",
        "lib/gitlab/middleware/basic_health_check.rb:25:in `call'",
        "lograge (0.11.2) lib/lograge/rails_ext/rack/logger.rb:15:in `call_app'",
        "railties (7.0.8.1) lib/rails/rack/logger.rb:25:in `block in call'",
        "activesupport (7.0.8.1) lib/active_support/tagged_logging.rb:99:in `block in tagged'",
        "activesupport (7.0.8.1) lib/active_support/tagged_logging.rb:37:in `tagged'",
        "activesupport (7.0.8.1) lib/active_support/tagged_logging.rb:99:in `tagged'",
        "railties (7.0.8.1) lib/rails/rack/logger.rb:25:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/remote_ip.rb:93:in `call'",
        "lib/gitlab/middleware/handle_ip_spoof_attack_error.rb:25:in `call'",
        "lib/gitlab/middleware/request_context.rb:15:in `call'",
        "lib/gitlab/middleware/webhook_recursion_detection.rb:15:in `call'",
        "request_store (1.5.1) lib/request_store/middleware.rb:19:in `call'",
        "rack (2.2.8.1) lib/rack/method_override.rb:24:in `call'",
        "rack (2.2.8.1) lib/rack/runtime.rb:22:in `call'",
        "rack-timeout (0.6.3) lib/rack/timeout/core.rb:148:in `block in call'",
        "rack-timeout (0.6.3) lib/rack/timeout/support/timeout.rb:19:in `timeout'",
        "rack-timeout (0.6.3) lib/rack/timeout/core.rb:147:in `call'",
        "config/initializers/fix_local_cache_middleware.rb:11:in `call'",
        "lib/gitlab/middleware/compressed_json.rb:44:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/executor.rb:14:in `call'",
        "lib/gitlab/middleware/rack_multipart_tempfile_factory.rb:19:in `call'",
        "rack (2.2.8.1) lib/rack/sendfile.rb:110:in `call'",
        "lib/gitlab/middleware/sidekiq_web_static.rb:20:in `call'",
        "lib/gitlab/metrics/requests_rack_middleware.rb:79:in `call'",
        "gitlab-labkit (0.36.0) lib/labkit/middleware/rack.rb:22:in `block in call'",
        "gitlab-labkit (0.36.0) lib/labkit/context.rb:35:in `with_context'",
        "gitlab-labkit (0.36.0) lib/labkit/middleware/rack.rb:21:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/request_id.rb:26:in `call'",
        "actionpack (7.0.8.1) lib/action_dispatch/middleware/host_authorization.rb:131:in `call'",
        "railties (7.0.8.1) lib/rails/engine.rb:530:in `call'",
        "railties (7.0.8.1) lib/rails/railtie.rb:226:in `public_send'",
        "railties (7.0.8.1) lib/rails/railtie.rb:226:in `method_missing'",
        "lib/gitlab/middleware/release_env.rb:13:in `call'",
        "rack (2.2.8.1) lib/rack/urlmap.rb:74:in `block in call'",
        "rack (2.2.8.1) lib/rack/urlmap.rb:58:in `each'",
        "rack (2.2.8.1) lib/rack/urlmap.rb:58:in `call'",
        "puma (6.4.0) lib/puma/configuration.rb:272:in `call'",
        "puma (6.4.0) lib/puma/request.rb:100:in `block in handle_request'",
        "puma (6.4.0) lib/puma/thread_pool.rb:378:in `with_force_shutdown'",
        "puma (6.4.0) lib/puma/request.rb:99:in `handle_request'",
        "puma (6.4.0) lib/puma/server.rb:443:in `process_client'",
        "puma (6.4.0) lib/puma/server.rb:241:in `block in run'",
        "puma (6.4.0) lib/puma/thread_pool.rb:155:in `block in spawn_thread'"
      ]
 }

Output of checks

This bug happens on GitLab.com

Possible fixes

Iteration Plan

  1. Wait for [Gitlab.com] Automatic removal of dormant members (&7533) to be complete. This will help remove billable members without having to leverage the API/UI.
  2. The actual longer term fix to the timeout problem when removing billable members will involve optimizing the members tables:
    1. #467281 (comment 2096212630)
    2. #467281 (comment 2082578926)
    3. #467281 (comment 2095765374)
  3. Once 2 is complete, then this problem outlined in this issue should no longer be a problem
Initial Iteration Plan

Phase 1: immediate customer concerns

  • backend: Create background job for billable member deletion
    • deduplicate the job (incase of deletion spam)
    • consider overall run time - we might want a limited capacity worker or to modify worker approach accordingly
  • backend: Modify API endpoint to support async
    • new param to indicate async
    • trigger async job
    • return appropriate response (e.g. a helpful message to explain delayed deletion)

Phase 2: UI (to be split into a separate issue and deferred until API is available)

  • frontend: Ensure front-end calls endpoint with async option
  • frontend: notification of async deletion
Edited by Alex Martin