【问题标题】:Cancelling a long running regex match?取消长时间运行的正则表达式匹配?
【发布时间】:2010-10-28 23:39:42
【问题描述】:

假设我正在运行一项服务,用户可以在其中提交正则表达式来搜索大量数据。如果用户提交了一个非常慢的正则表达式(即 Matcher.find() 需要几分钟才能返回),我想要一种取消该匹配的方法。我能想到的唯一方法是让另一个线程监视匹配需要多长时间,并在必要时使用 Thread.stop() 取消它。

成员变量:

long REGEX_TIMEOUT = 30000L;
Object lock = new Object();
boolean finished = false;
Thread matcherThread;

匹配线程:

try {
    matcherThread = Thread.currentThread();

    // imagine code to start monitor thread is here

    try {
        matched = matcher.find();
    } finally {
        synchronized (lock) {
            finished = true;
            lock.notifyAll();
        }
    }
} catch (ThreadDeath td) {
    // send angry message to client
    // handle error without rethrowing td
}

监控线程:

synchronized (lock) {
    while (! finished) {
        try {
            lock.wait(REGEX_TIMEOUT);

            if (! finished) {
                matcherThread.stop();
            }
        } catch (InterruptedException ex) {
            // ignore, top level method in dedicated thread, etc..
        }
    }
}

我已阅读 java.sun.com/j2se/1.4.2/docs/guide/misc/threadPrimitiveDeprecation.html 并且我认为这种用法是安全的,因为我正在控制通过同步抛出 ThreadDeath 的位置并处理它和唯一损坏的对象可能是我的 Pattern 和 Matcher 实例,它们无论如何都会被丢弃。我认为这会破坏 Thread.stop() 因为我没有重新抛出错误,但我真的不希望线程死掉,只是中止 find() 方法。

到目前为止,我已经设法避免使用这些已弃用的 API 组件,但 Matcher.find() 似乎不是可中断的,并且可能需要很长时间才能返回。有没有更好的方法来做到这一点?

【问题讨论】:

  • 就个人而言,我认为允许用户提交正则表达式作为搜索条件是一个坏主意。可能是程序员,但不是最终用户……
  • 当然,如果您接受任意正则表达式,您应该期望得到 DoSed。
  • 并非所有代码都暴露在公共网络中,您必须担心 DoS。

标签: java regex multithreading


【解决方案1】:

来自 Heritrix:(crawler.archive.org)

/**
 * CharSequence that noticed thread interrupts -- as might be necessary 
 * to recover from a loose regex on unexpected challenging input. 
 * 
 * @author gojomo
 */
public class InterruptibleCharSequence implements CharSequence {
    CharSequence inner;
    // public long counter = 0; 

    public InterruptibleCharSequence(CharSequence inner) {
        super();
        this.inner = inner;
    }

    public char charAt(int index) {
        if (Thread.interrupted()) { // clears flag if set
            throw new RuntimeException(new InterruptedException());
        }
        // counter++;
        return inner.charAt(index);
    }

    public int length() {
        return inner.length();
    }

    public CharSequence subSequence(int start, int end) {
        return new InterruptibleCharSequence(inner.subSequence(start, end));
    }

    @Override
    public String toString() {
        return inner.toString();
    }
}

用这个包裹你的 CharSequence,线程中断将起作用......

【讨论】:

  • 如果将异常位移出 charAt 会稍微快一些,但真正的问题可能是低效模式而不是大目标文本。
  • 众所周知,如果指定了某些邪恶模式,所有回溯正则表达式算法都会变得病态。正如汤姆所说,读取输入字符串不太可能导致大量的正则表达式运行时间,但更可能是因为正则表达式算法中的过度回溯。可能是正则表达式在读取另一个字符之前可能会卡住很长时间(我以前在 perl 中看到过)所以中断可能在一段时间内不起作用。
  • @Kris charAt 永远不会被调用。可能是什么问题呢?请在这里查看我的问题stackoverflow.com/questions/17674839/…
  • 我同意,这行不通。正则表达式匹配器将字符序列复制到字符串。
  • @Benj 当前的 Java 实现的 Matcher 实际上确实会再次调用 charAt,即使在回溯时,即使在小字符串中 carAt 被命中数百万次。刚做了一个测试。在 2,3 秒内使用字符串 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 和模式 (x+x+)+y 它调用了 charAt 9000 万次(没有先加热。我认为按照微基准测试指南在适当的环境中进行测试会在给定时期)。
【解决方案2】:

稍加改动就可以避免为此使用额外的线程:

public class RegularExpressionUtils {

    // demonstrates behavior for regular expression running into catastrophic backtracking for given input
    public static void main(String[] args) {
        Matcher matcher = createMatcherWithTimeout(
                "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "(x+x+)+y", 2000);
        System.out.println(matcher.matches());
    }

    public static Matcher createMatcherWithTimeout(String stringToMatch, String regularExpression, int timeoutMillis) {
        Pattern pattern = Pattern.compile(regularExpression);
        return createMatcherWithTimeout(stringToMatch, pattern, timeoutMillis);
    }

    public static Matcher createMatcherWithTimeout(String stringToMatch, Pattern regularExpressionPattern, int timeoutMillis) {
        CharSequence charSequence = new TimeoutRegexCharSequence(stringToMatch, timeoutMillis, stringToMatch,
                regularExpressionPattern.pattern());
        return regularExpressionPattern.matcher(charSequence);
    }

    private static class TimeoutRegexCharSequence implements CharSequence {

        private final CharSequence inner;

        private final int timeoutMillis;

        private final long timeoutTime;

        private final String stringToMatch;

        private final String regularExpression;

        public TimeoutRegexCharSequence(CharSequence inner, int timeoutMillis, String stringToMatch, String regularExpression) {
            super();
            this.inner = inner;
            this.timeoutMillis = timeoutMillis;
            this.stringToMatch = stringToMatch;
            this.regularExpression = regularExpression;
            timeoutTime = System.currentTimeMillis() + timeoutMillis;
        }

        public char charAt(int index) {
            if (System.currentTimeMillis() > timeoutTime) {
                throw new RuntimeException("Timeout occurred after " + timeoutMillis + "ms while processing regular expression '"
                                + regularExpression + "' on input '" + stringToMatch + "'!");
            }
            return inner.charAt(index);
        }

        public int length() {
            return inner.length();
        }

        public CharSequence subSequence(int start, int end) {
            return new TimeoutRegexCharSequence(inner.subSequence(start, end), timeoutMillis, stringToMatch, regularExpression);
        }

        @Override
        public String toString() {
            return inner.toString();
        }
    }

}

非常感谢 dawce 为我指出这个解决方案以回答不必要的复杂 question

【讨论】:

  • +1 建议:currentTimeMillis() 是一项相当昂贵的操作。添加一个计数器,并且仅在每 N 次调用 charAt() 时调用它。
  • 很好的答案。任何使用它的人都希望抛出自定义异常而不是 RuntimeException。
  • 是的,这确实是一个很好的解决方案,但仅限于抛出 RTE。如果我想发送我的自定义异常,我认为我们需要在 TimeoutRegexCharSequence 类之上再创建一个包装器。
【解决方案3】:

另一种解决方法是限制匹配器的region,然后调用find(),重复直到线程被中断或找到匹配。

【讨论】:

  • 只有当你能保证你的潜在匹配的大小和边界时。这可能不适用于围绕任意表达的问题。
【解决方案4】:

也许您需要一个新的库来实现 NFA 算法。

NFA算法比Java标准库使用的算法快数百倍。

而且 Java 标准库对输入正则表达式很敏感,这可能会导致您的问题发生——一些输入会使 CPU 运行多年。

并且可以通过 NFA 算法通过它使用的步骤来设置超时。它比 Thread 解决方案有效。相信我,我使用线程超时来解决一个相对问题,这对性能来说太可怕了。我终于通过修改我的算法实现的主循环来解决这个问题。我在主循环中插入了一些检查点来测试时间。

详情可以在这里找到:https://swtch.com/~rsc/regexp/regexp1.html

【讨论】:

    【解决方案5】:

    我加入了一个计数器来检查每 n 次 charAt 读取,以减少开销。

    注意事项:

    有人说 carAt 的调用频率可能不够高。我刚刚添加了 foo 变量,以说明调用了多少 charAt,并且它足够频繁。如果您要在生产中使用它,请删除该计数器,因为如果长时间在服务器中运行,它会降低性能并最终导致长时间溢出。在这个例子中,charAt 每 0.8 秒左右被调用 3000 万次(没有在适当的微基准测试条件下进行测试,这只是一个概念证明)。如果您想要更高的精度,您可以设置较低的 checkInterval,但会以性能为代价(从长远来看,System.currentTimeMillis() > timeoutTime 比 if 子句更昂贵。

    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    import com.goikosoft.test.RegexpTimeoutException;
    
    /**
     * Allows to create timeoutable regular expressions.
     *
     * Limitations: Can only throw RuntimeException. Decreases performance.
     *
     * Posted by Kris in stackoverflow.
     *
     * Modified by dgoiko to  ejecute timeout check only every n chars.
     * Now timeout < 0 means no timeout.
     *
     * @author Kris https://stackoverflow.com/a/910798/9465588
     *
     */
    public class RegularExpressionUtils {
    
        public static long foo = 0;
    
        // demonstrates behavior for regular expression running into catastrophic backtracking for given input
        public static void main(String[] args) {
            long millis = System.currentTimeMillis();
            // This checkInterval produces a < 500 ms delay. Higher checkInterval will produce higher delays on timeout.
            Matcher matcher = createMatcherWithTimeout(
                    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "(x+x+)+y", 10000, 30000000);
            try {
                System.out.println(matcher.matches());
            } catch (RuntimeException e) {
                System.out.println("Operation timed out after " + (System.currentTimeMillis() - millis) + " milliseconds");
            }
            System.out.print(foo);
        }
    
        public static Matcher createMatcherWithTimeout(String stringToMatch, String regularExpression, long timeoutMillis,
                                                          int checkInterval) {
            Pattern pattern = Pattern.compile(regularExpression);
            return createMatcherWithTimeout(stringToMatch, pattern, timeoutMillis, checkInterval);
        }
    
        public static Matcher createMatcherWithTimeout(String stringToMatch, Pattern regularExpressionPattern,
                                                        long timeoutMillis, int checkInterval) {
            if (timeoutMillis < 0) {
                return regularExpressionPattern.matcher(stringToMatch);
            }
            CharSequence charSequence = new TimeoutRegexCharSequence(stringToMatch, timeoutMillis, stringToMatch,
                    regularExpressionPattern.pattern(), checkInterval);
            return regularExpressionPattern.matcher(charSequence);
        }
    
        private static class TimeoutRegexCharSequence implements CharSequence {
    
            private final CharSequence inner;
    
            private final long timeoutMillis;
    
            private final long timeoutTime;
    
            private final String stringToMatch;
    
            private final String regularExpression;
    
            private int checkInterval;
    
            private int attemps;
    
            TimeoutRegexCharSequence(CharSequence inner, long timeoutMillis, String stringToMatch,
                                      String regularExpression, int checkInterval) {
                super();
                this.inner = inner;
                this.timeoutMillis = timeoutMillis;
                this.stringToMatch = stringToMatch;
                this.regularExpression = regularExpression;
                timeoutTime = System.currentTimeMillis() + timeoutMillis;
                this.checkInterval = checkInterval;
                this.attemps = 0;
            }
    
            public char charAt(int index) {
                if (this.attemps == this.checkInterval) {
                    foo++;
                    if (System.currentTimeMillis() > timeoutTime) {
                        throw new RegexpTimeoutException(regularExpression, stringToMatch, timeoutMillis);
                    }
                    this.attemps = 0;
                } else {
                    this.attemps++;
                }
    
                return inner.charAt(index);
            }
    
            public int length() {
                return inner.length();
            }
    
            public CharSequence subSequence(int start, int end) {
                return new TimeoutRegexCharSequence(inner.subSequence(start, end), timeoutMillis, stringToMatch,
                                                    regularExpression, checkInterval);
            }
    
            @Override
            public String toString() {
                return inner.toString();
            }
        }
    
    }
    

    还有自定义异常,所以你可以只捕获那个异常以避免吞下其他 RE 模式/匹配器可能抛出的异常。

    public class RegexpTimeoutException extends RuntimeException {
        private static final long serialVersionUID = 6437153127902393756L;
    
        private final String regularExpression;
    
        private final String stringToMatch;
    
        private final long timeoutMillis;
    
        public RegexpTimeoutException() {
            super();
            regularExpression = null;
            stringToMatch = null;
            timeoutMillis = 0;
        }
    
        public RegexpTimeoutException(String message, Throwable cause) {
            super(message, cause);
            regularExpression = null;
            stringToMatch = null;
            timeoutMillis = 0;
        }
    
        public RegexpTimeoutException(String message) {
            super(message);
            regularExpression = null;
            stringToMatch = null;
            timeoutMillis = 0;
        }
    
        public RegexpTimeoutException(Throwable cause) {
            super(cause);
            regularExpression = null;
            stringToMatch = null;
            timeoutMillis = 0;
        }
    
        public RegexpTimeoutException(String regularExpression, String stringToMatch, long timeoutMillis) {
            super("Timeout occurred after " + timeoutMillis + "ms while processing regular expression '"
                    + regularExpression + "' on input '" + stringToMatch + "'!");
            this.regularExpression = regularExpression;
            this.stringToMatch = stringToMatch;
            this.timeoutMillis = timeoutMillis;
        }
    
        public String getRegularExpression() {
            return regularExpression;
        }
    
        public String getStringToMatch() {
            return stringToMatch;
        }
    
        public long getTimeoutMillis() {
            return timeoutMillis;
        }
    
    }
    

    基于Andreas' answer。主要功劳应该归于他和他的来源。

    【讨论】:

      【解决方案6】:

      可以使用以下方法停止长时间运行的模式匹配过程。

      • 创建管理模式匹配状态的StateFulCharSequence 类。当该状态发生变化时,下次调用 charAt 方法时会引发异常。
      • 可以使用 ScheduledExecutorService 安排状态更改,并设置所需的超时时间。
      • 这里的模式匹配发生在主线程中,不需要每次都检查线程中断状态。

        public class TimedPatternMatcher {
        public static void main(String[] args) {
            ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
            Pattern pattern = Pattern.compile("some regex pattern");
            StateFulCharSequence stateFulCharSequence = new StateFulCharSequence("some character sequence");
            Matcher matcher = pattern.matcher(stateFulCharSequence);
            executorService.schedule(stateFulCharSequence, 10, TimeUnit.MILLISECONDS);
            try {
                boolean isMatched = matcher.find();
            }catch (Exception e) {
                e.printStackTrace();
            }
        
        }
        
        /*
        When this runnable is executed, it will set timeOut to true and pattern matching is stopped by throwing exception.
         */
        public static class StateFulCharSequence implements CharSequence, Runnable{
            private CharSequence inner;
        
            private boolean isTimedOut = false;
        
            public StateFulCharSequence(CharSequence inner) {
                super();
                this.inner = inner;
            }
        
            public char charAt(int index) {
                if (isTimedOut) {
                    throw new RuntimeException(new TimeoutException("Pattern matching timeout occurs"));
                }
                return inner.charAt(index);
            }
        
            @Override
            public int length() {
                return inner.length();
            }
        
            @Override
            public CharSequence subSequence(int start, int end) {
                return new StateFulCharSequence(inner.subSequence(start, end));
            }
        
            @Override
            public String toString() {
                return inner.toString();
            }
        
            public void setTimedOut() {
                this.isTimedOut = true;
            }
        
            @Override
            public void run() {
                this.isTimedOut = true;
            }
        }}
        

      【讨论】:

      • 这个解决方案并不能真正解决问题,如果charAt需要很长时间怎么办?所以你不能保证定义的超时
      • @lssilva charAt 每秒调用数百万次,即使在短字符串中也是如此。如果您不信任我,请自行测试。
      • @DGoiko 我刚刚做了一个测试,创建一个生成随机字符串的方法(geeksforgeeks.org/generate-random-string-of-given-size-in-java)并运行代码:println(System.currentTimeMillis()) println(getAlphaNumericString(Int.MaxValue - 1000).charAt(900000)) println(System.currentTimeMillis()) 花了将近 1 分钟
      • @lssilva 我的意思是阅读正则表达式的字符。从stackoverflow.com/a/11348374/9465588 获取代码并在 charAt 中设置一个计数器,以查看在所需时间内读取了多少次。你可以用它测试任何你能想到的长时间运行的正则表达式。
      【解决方案7】:

      如何在使用一个或多个正则表达式模式执行之前检查用户提交的正则表达式是否存在“邪恶”模式(这可能是在条件执行正则表达式之前调用的方法):

      这个正则表达式:

      \(.+\+\)[\+\*]
      

      将匹配:

      (a+)+
      (ab+)+
      ([a-zA-Z]+)*
      

      这个正则表达式:

      \((.+)\|(\1\?|\1{2,})\)\+
      

      将匹配:

      (a|aa)+
      (a|a?)+
      

      这个正则表达式:

      \(\.\*.\)\{\d{2,}\}
      

      将匹配:

      (.*a){x} for x \> 10
      

      我可能对 Regex 和 Regex DoS 有点天真,但我不禁认为,对已知的“邪恶”模式进行一些预筛选将大大有助于防止执行时出现问题,尤其是如果有问题的正则表达式是最终用户提供的输入。上面的模式可能不够完善,因为我远非正则表达式专家。这只是深思熟虑,因为我在那里发现的所有其他内容似乎都表明它无法完成,并且专注于在正则表达式引擎上设置超时,或者限制允许执行的迭代次数.

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-06
        • 2023-04-06
        相关资源
        最近更新 更多