View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.filter;
17  
18  import java.io.IOException;
19  import java.util.Arrays;
20  import java.util.Set;
21  import java.util.stream.Collectors;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.fess.mylasta.direction.FessConfig;
26  import org.codelibs.fess.util.ComponentUtil;
27  
28  import jakarta.servlet.Filter;
29  import jakarta.servlet.FilterChain;
30  import jakarta.servlet.ServletException;
31  import jakarta.servlet.ServletRequest;
32  import jakarta.servlet.ServletResponse;
33  import jakarta.servlet.http.HttpServletRequest;
34  import jakarta.servlet.http.HttpServletResponse;
35  
36  /**
37   * Filter for CPU load-based request control.
38   * Returns HTTP 429 (Too Many Requests) when CPU usage exceeds configurable thresholds.
39   * Web and API requests have independent threshold settings.
40   */
41  public class LoadControlFilter implements Filter {
42  
43      private static final Logger logger = LogManager.getLogger(LoadControlFilter.class);
44  
45      private static final int RETRY_AFTER_SECONDS = 60;
46  
47      private static final Set<String> STATIC_EXTENSIONS =
48              Arrays.stream(new String[] { ".css", ".js", ".png", ".jpg", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot" })
49                      .collect(Collectors.toSet());
50  
51      /**
52       * Creates a new instance of LoadControlFilter.
53       */
54      public LoadControlFilter() {
55          // Default constructor
56      }
57  
58      @Override
59      public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
60              throws IOException, ServletException {
61          if (!ComponentUtil.available()) {
62              chain.doFilter(request, response);
63              return;
64          }
65  
66          final HttpServletRequest httpRequest = (HttpServletRequest) request;
67          final HttpServletResponse httpResponse = (HttpServletResponse) response;
68          final String path = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
69  
70          if (isExcludedPath(path)) {
71              chain.doFilter(request, response);
72              return;
73          }
74  
75          final boolean isApiPath = path.startsWith("/api/");
76          final FessConfig fessConfig = ComponentUtil.getFessConfig();
77          final int threshold = isApiPath ? fessConfig.getApiLoadControlAsInteger() : fessConfig.getWebLoadControlAsInteger();
78  
79          if (threshold >= 100) {
80              chain.doFilter(request, response);
81              return;
82          }
83  
84          final short cpuPercent = ComponentUtil.getSystemHelper().getSearchEngineCpuPercent();
85  
86          if (cpuPercent < threshold) {
87              chain.doFilter(request, response);
88              return;
89          }
90  
91          if (logger.isInfoEnabled()) {
92              logger.info("Rejecting request due to high CPU load: path={}, cpu={}%, threshold={}%", path, cpuPercent, threshold);
93          }
94  
95          if (isApiPath) {
96              sendApiResponse(httpResponse);
97          } else {
98              httpResponse.sendError(429);
99          }
100     }
101 
102     /**
103      * Checks if the given path should be excluded from load control.
104      * @param path the request path
105      * @return true if the path should be excluded
106      */
107     protected boolean isExcludedPath(final String path) {
108         if (path.startsWith("/admin") || path.startsWith("/error") || path.startsWith("/login")) {
109             return true;
110         }
111         final int dotIndex = path.lastIndexOf('.');
112         if (dotIndex >= 0) {
113             final String extension = path.substring(dotIndex);
114             return STATIC_EXTENSIONS.contains(extension);
115         }
116         return false;
117     }
118 
119     /**
120      * Sends a 429 JSON response for API requests.
121      * @param response the HTTP response
122      * @throws IOException if an I/O error occurs
123      */
124     protected void sendApiResponse(final HttpServletResponse response) throws IOException {
125         response.setStatus(429);
126         response.setContentType("application/json;charset=UTF-8");
127         response.setHeader("Retry-After", String.valueOf(RETRY_AFTER_SECONDS));
128         response.getWriter()
129                 .write("{\"response\":{\"status\":9,\"message\":\"Server is busy. Please retry after " + RETRY_AFTER_SECONDS
130                         + " seconds.\",\"retry_after\":" + RETRY_AFTER_SECONDS + "}}");
131     }
132 }