├── .codecov.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── .travis.yml ├── LICENSE.md ├── README.md ├── docs ├── bloom-filter-swagger.yaml ├── dist │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map └── index.html ├── filter-service-core ├── bin │ ├── check-and-set-benchmark.sh │ ├── check-benchmark.sh │ ├── create-filter-benchmark.sh │ └── filter-service ├── config │ └── configuration.yaml ├── distribution.xml ├── pom.xml ├── scripts │ ├── check-and-set-benchmark.lua │ ├── check-benchmark.lua │ ├── create-filter-benchmark.lua │ └── filter_service_api.sh └── src │ ├── main │ ├── java │ │ └── cn │ │ │ └── leancloud │ │ │ └── filter │ │ │ └── service │ │ │ ├── AbstractBloomFilterConfig.java │ │ │ ├── BackgroundJobScheduler.java │ │ │ ├── BadParameterException.java │ │ │ ├── BloomFilter.java │ │ │ ├── BloomFilterConfig.java │ │ │ ├── BloomFilterFactory.java │ │ │ ├── BloomFilterHttpService.java │ │ │ ├── BloomFilterManager.java │ │ │ ├── BloomFilterManagerImpl.java │ │ │ ├── BloomFilterManagerListener.java │ │ │ ├── Bootstrap.java │ │ │ ├── Configuration.java │ │ │ ├── CountUpdateBloomFilterFactory.java │ │ │ ├── CountUpdateBloomFilterWrapper.java │ │ │ ├── DefaultMetricsService.java │ │ │ ├── Errors.java │ │ │ ├── ExpirableBloomFilter.java │ │ │ ├── ExpirableBloomFilterConfig.java │ │ │ ├── FilterNotFoundException.java │ │ │ ├── FilterRecord.java │ │ │ ├── FilterRecordInputStream.java │ │ │ ├── FilterServiceFileUtils.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── GuavaBloomFilter.java │ │ │ ├── GuavaBloomFilterFactory.java │ │ │ ├── InvalidBloomFilterPurgatory.java │ │ │ ├── InvalidFilterException.java │ │ │ ├── Listenable.java │ │ │ ├── NonNullByDefault.java │ │ │ ├── PersistentFiltersJob.java │ │ │ ├── PersistentManager.java │ │ │ ├── PersistentStorageException.java │ │ │ ├── Purgatory.java │ │ │ ├── PurgeFiltersJob.java │ │ │ ├── ServerOptions.java │ │ │ ├── ServiceParameterPreconditions.java │ │ │ ├── UnfinishedFilterException.java │ │ │ ├── package-info.java │ │ │ └── utils │ │ │ ├── AbstractIterator.java │ │ │ ├── ChecksumedBufferedOutputStream.java │ │ │ ├── Crc32C.java │ │ │ ├── DefaultTimer.java │ │ │ ├── Java.java │ │ │ ├── PureJavaCrc32C.java │ │ │ ├── Timer.java │ │ │ └── package-info.java │ └── resources │ │ └── log4j2.xml │ └── test │ ├── java │ └── cn │ │ └── leancloud │ │ └── filter │ │ └── service │ │ ├── AbstractBloomFilterConfigTest.java │ │ ├── AdjustableTimer.java │ │ ├── BackgroundJobSchedulerTest.java │ │ ├── BloomFilterHttpServiceTest.java │ │ ├── BloomFilterManagerImplTest.java │ │ ├── BootstrapTest.java │ │ ├── ConfigurationTest.java │ │ ├── CountUpdateBloomFilterFactoryTest.java │ │ ├── CountUpdateBloomFilterWrapperTest.java │ │ ├── ErrorsTest.java │ │ ├── ExpirableBloomFilterConfigTest.java │ │ ├── FilterRecordInputStreamTest.java │ │ ├── FilterRecordTest.java │ │ ├── FilterServiceFileUtilsTest.java │ │ ├── GuavaBloomFilterTest.java │ │ ├── InvalidBloomFilterPurgatoryTest.java │ │ ├── PersistentFilterByOutputStreamBenchmark.java │ │ ├── PersistentFiltersJobTest.java │ │ ├── PersistentManagerTest.java │ │ ├── PurgeFiltersJobTest.java │ │ ├── ServerOptionsTest.java │ │ ├── TestingUtils.java │ │ └── utils │ │ ├── AbstractIteratorTest.java │ │ ├── Crc32CTest.java │ │ └── JavaTest.java │ └── resources │ ├── empty-configuration.yaml │ ├── illegal-configuration.yaml │ └── testing-configuration.yaml ├── filter-service-metrics ├── pom.xml └── src │ └── main │ └── java │ └── cn │ └── leancloud │ └── filter │ └── service │ └── metrics │ └── MetricsService.java ├── mvnw ├── mvnw.cmd └── pom.xml /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 0.1% 6 | patch: off 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | target/ 13 | logs/ 14 | .idea/ 15 | 16 | # Package Files # 17 | *.jar 18 | *.war 19 | *.nar 20 | *.ear 21 | *.zip 22 | *.tar.gz 23 | *.rar 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | *.iml 29 | lock 30 | snapshot.db 31 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 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, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | cache: 4 | directories: 5 | - $HOME/.m2 6 | 7 | install: 8 | - mvn clean install -Pci-install -B -U -e 9 | 10 | script: 11 | - travis_retry mvn clean package -Pci-test 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2019 Rui Guo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /docs/dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leancloud/filter-service/96004280cc9aad76461f2f3af7285b09225d9f4c/docs/dist/favicon-16x16.png -------------------------------------------------------------------------------- /docs/dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leancloud/filter-service/96004280cc9aad76461f2f3af7285b09225d9f4c/docs/dist/favicon-32x32.png -------------------------------------------------------------------------------- /docs/dist/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swagger UI: OAuth2 Redirect 4 | 5 | 6 | 7 | 69 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /filter-service-core/bin/check-and-set-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=$(dirname $0)/.. 4 | . $BASE_DIR/scripts/filter_service_api.sh --source-only 5 | 6 | create_filter check-set-bench 100000000 0.001 7 | 8 | wrk --threads 4 --connections 20 --duration 30s --latency --script $BASE_DIR/scripts/check-and-set-benchmark.lua http://127.0.0.1:8080/ 9 | 10 | delete_filter check-set-bench 11 | -------------------------------------------------------------------------------- /filter-service-core/bin/check-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=$(dirname $0)/.. 4 | . $BASE_DIR/scripts/filter_service_api.sh --source-only 5 | 6 | create_filter check-bench 100000000 0.001 7 | 8 | wrk --threads 4 --connections 20 --duration 30s --latency --script $BASE_DIR/scripts/check-benchmark.lua http://127.0.0.1:8080/ 9 | 10 | delete_filter check-bench 11 | -------------------------------------------------------------------------------- /filter-service-core/bin/create-filter-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=$(dirname $0)/.. 4 | . $BASE_DIR/scripts/filter_service_api.sh --source-only 5 | 6 | wrk --threads 4 --connections 20 --duration 30s --latency --script $BASE_DIR/scripts/create-filter-benchmark.lua http://127.0.0.1:8080/ 7 | -------------------------------------------------------------------------------- /filter-service-core/bin/filter-service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=$(dirname $0)/.. 4 | CLASSPATH=$(echo $BASE_DIR/*.jar $BASE_DIR/target/*.jar | tr ' ' ':') 5 | 6 | # Which java to use 7 | if [ -z "$JAVA_HOME" ]; then 8 | JAVA="java" 9 | else 10 | JAVA="$JAVA_HOME/bin/java" 11 | fi 12 | 13 | JAVA_VERSION=$($JAVA -version 2>&1 >/dev/null | grep 'version' | awk '{print $3}') 14 | echo "Using java version:$JAVA_VERSION path:$JAVA" 15 | 16 | # Log directory to use 17 | if [ "x$LOG_DIR" = "x" ]; then 18 | LOG_DIR="$BASE_DIR/logs" 19 | fi 20 | 21 | if [ ! -d "$LOG_DIR" ]; then 22 | mkdir -p $LOG_DIR 23 | fi 24 | 25 | # Log4j settings 26 | if [ -z "$FILTER_SERVICE_LOG4J_OPTS" ]; then 27 | # use the log4j2.xml in $BASE_DIR or internal log4j2.xml to log 28 | FILTER_SERVICE_LOG4J_OPTS="-Dlog4j2.AsyncQueueFullPolicy=Discard -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j.logdir=$LOG_DIR" 29 | fi 30 | 31 | # GC logs 32 | if [ -z "$FILTER_SERVICE_JVM_GC_LOG_CONFIGS" ]; then 33 | if [[ $JAVA_VERSION =~ ^\"1[1-9]+.* ]]; then 34 | FILTER_SERVICE_JVM_GC_LOG_CONFIGS="-Xlog:gc+heap=info:file=$LOG_DIR/gc.log:time,level,tags:filecount=5,filesize=10240" 35 | else 36 | FILTER_SERVICE_JVM_GC_LOG_CONFIGS="-XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:+PrintGCDetails -XX:+PrintReferenceGC -Xloggc:$LOG_DIR/gc.log" 37 | fi 38 | fi 39 | 40 | # Memory options. 41 | # Please set a large MaxDirectMemorySize if you need to create a big Bloom filter with lots of expected insertions and 42 | # a very low false positive possibility, otherwise you might got direct buffer OOM. 43 | # Please set a reasonable value for jdk.nio.maxCachedBufferSize to prevent the thread doing IO operations try to cache 44 | # too many big direct buffer which might cause direct buffer OOM too. 45 | if [ -z "$FILTER_SERVICE_HEAP_OPTS" ]; then 46 | FILTER_SERVICE_HEAP_OPTS="-Xmx1G -Xms1G -XX:MaxDirectMemorySize=256M -Djdk.nio.maxCachedBufferSize=16777216" 47 | fi 48 | 49 | # JVM performance options 50 | if [ -z "$FILTER_SERVICE_JVM_PERFORMANCE_OPTS" ]; then 51 | FILTER_SERVICE_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC" 52 | fi 53 | 54 | $JAVA \ 55 | $FILTER_SERVICE_HEAP_OPTS \ 56 | $FILTER_SERVICE_JVM_PERFORMANCE_OPTS \ 57 | $FILTER_SERVICE_JVM_GC_LOG_CONFIGS \ 58 | $FILTER_SERVICE_LOG4J_OPTS \ 59 | -cp $CLASSPATH \ 60 | cn.leancloud.filter.service.Bootstrap \ 61 | $@ 62 | 63 | -------------------------------------------------------------------------------- /filter-service-core/config/configuration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # the prefix for all the metrics generated from micrometer 3 | metricsPrefix: "filterService" 4 | 5 | # the default expected insertions to filter. If no value provided on creating filter, this value will be used 6 | defaultExpectedInsertions: 1000000 7 | 8 | # the default desired false positive probability for a filter. If no value provided on creating filter, this value will be used 9 | defaultFalsePositiveProbability: 0.01 10 | 11 | # the default valid seconds after create for a filter. If no value provided on creating filter, this value will be used 12 | defaultValidSecondsAfterCreate: 86400 13 | 14 | # maximum allowed http/https concurrent connections 15 | maxHttpConnections: 1000 16 | 17 | # maximum allowed length of the content decoded at the session layer. the default value is 10 MB 18 | maxHttpRequestLength: 10485760 19 | 20 | # the default timeout of a request 21 | requestTimeoutSeconds: 5 22 | 23 | # the idle timeout of a connection in milliseconds for keep-alive 24 | idleTimeoutMillis: 10000 25 | 26 | # maximum thread pool size to execute internal potential long running tasks 27 | maxWorkerThreadPoolSize: 10 28 | 29 | # the configuration options for every underlying TCP socket 30 | channelOptions: 31 | SO_BACKLOG: 2048 32 | SO_RCVBUF: 2048 33 | SO_SNDBUF: 2048 34 | TCP_NODELAY: true 35 | 36 | # the interval for the purge thread to scan all the filters to find and clean expired filters 37 | purgeFilterIntervalMillis: 300 38 | 39 | # config when to save all the filters on disk. Will save the filters if both the given number of seconds and the given 40 | # number of update operations against the service occurred. 41 | # In the example below the behaviour will be to save: 42 | # after 900 sec if at least 1 filter update operation occurred 43 | # after 300 sec if at least 100 filter update operation occurred 44 | # after 60 sec if at least 10000 filter update operation occurred 45 | triggerPersistenceCriteria: 46 | - periodInSeconds: 900 47 | updatesMoreThan: 1 48 | - periodInSeconds: 300 49 | updatesMoreThan: 100 50 | - periodInSeconds: 60 51 | updatesMoreThan: 10000 52 | 53 | # the path to a directory to store persistent file. Leave it empty to use the path to "user.dir" system property 54 | persistentStorageDirectory: ~ 55 | 56 | # 100KB 57 | channelBufferSizeForFilterPersistence: 102400 58 | 59 | # when this switch is on, we try to recover filters from a corrupted or unfinished persistent file as many filters as we can; 60 | # otherwise, we will throw an exception when we make sure that the persistent file is corrupted or unfinished. 61 | allowRecoverFromCorruptedPersistentFile: true 62 | 63 | # the number of milliseconds to wait for active requests to go end before shutting down. 0 means the server 64 | # will stop right away without waiting 65 | gracefulShutdownQuietPeriodMillis: 0 66 | 67 | # the number of milliseconds to wait before shutting down the server regardless of active requests. 68 | # This should be set to a time greater than gracefulShutdownQuietPeriodMillis to ensure the server 69 | # shuts down even if there is a stuck request. 70 | gracefulShutdownTimeoutMillis: 0 71 | -------------------------------------------------------------------------------- /filter-service-core/distribution.xml: -------------------------------------------------------------------------------- 1 | 4 | distribution 5 | 6 | dir 7 | tar.gz 8 | 9 | true 10 | 11 | 12 | unix 13 | 0755 14 | 15 | bin/** 16 | scripts/** 17 | config/** 18 | 19 | 20 | **/src/** 21 | **/target/** 22 | **/.*/** 23 | 24 | 25 | 26 | 27 | 28 | ${project.build.directory}/filter-service.jar 29 | ${file.separator} 30 | 31 | 32 | -------------------------------------------------------------------------------- /filter-service-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | filter-service 5 | cn.leancloud 6 | 1.16-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | filter-service-core 11 | jar 12 | filter-service-core ${project.version} 13 | 14 | 15 | 16 | cn.leancloud 17 | filter-service-metrics 18 | 19 | 20 | com.linecorp.armeria 21 | armeria 22 | 23 | 24 | com.lmax 25 | disruptor 26 | 27 | 28 | com.google.guava 29 | guava 30 | 31 | 32 | org.slf4j 33 | slf4j-api 34 | 35 | 36 | org.apache.logging.log4j 37 | log4j-slf4j-impl 38 | 39 | 40 | org.apache.logging.log4j 41 | log4j-core 42 | 43 | 44 | com.google.code.findbugs 45 | jsr305 46 | 47 | 48 | commons-io 49 | commons-io 50 | 51 | 52 | io.dropwizard.metrics 53 | metrics-core 54 | 55 | 56 | slf4j-api 57 | org.slf4j 58 | 59 | 60 | 61 | 62 | info.picocli 63 | picocli 64 | 65 | 66 | com.fasterxml.jackson.dataformat 67 | jackson-dataformat-yaml 68 | 69 | 70 | 71 | org.assertj 72 | assertj-core 73 | test 74 | 75 | 76 | org.awaitility 77 | awaitility 78 | test 79 | 80 | 81 | junit 82 | junit 83 | test 84 | 85 | 86 | org.mockito 87 | mockito-core 88 | test 89 | 90 | 91 | org.openjdk.jmh 92 | jmh-core 93 | test 94 | 95 | 96 | org.openjdk.jmh 97 | jmh-generator-annprocess 98 | test 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-assembly-plugin 107 | 108 | 109 | jar-with-dependencies 110 | 111 | 112 | 113 | true 114 | 115 | 116 | filter-service 117 | false 118 | 119 | 120 | 121 | assemble-all 122 | package 123 | 124 | single 125 | 126 | 127 | 128 | assemble-distribution 129 | package 130 | 131 | single 132 | 133 | 134 | 135 | distribution.xml 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /filter-service-core/scripts/check-and-set-benchmark.lua: -------------------------------------------------------------------------------- 1 | 2 | unique_thread_id = tostring( {} ):sub(8) 3 | testing_value = 0 4 | request = function() 5 | testing_value = testing_value + 1 6 | path = "/v1/bloomfilter/check-set-bench/check-and-set" 7 | wrk.headers["content-type"] = "application/json; charset=utf-8" 8 | return wrk.format("POST", path, nil, "{\"value\": \"" .. testing_value .. "\"}") 9 | end 10 | -------------------------------------------------------------------------------- /filter-service-core/scripts/check-benchmark.lua: -------------------------------------------------------------------------------- 1 | 2 | unique_thread_id = tostring( {} ):sub(8) 3 | testing_value = 0 4 | request = function() 5 | testing_value = testing_value + 1 6 | path = "/v1/bloomfilter/check-bench/check" 7 | wrk.headers["content-type"] = "application/json; charset=utf-8" 8 | return wrk.format("POST", path, nil, "{\"value\": \"" .. testing_value .. "\"}") 9 | end 10 | -------------------------------------------------------------------------------- /filter-service-core/scripts/create-filter-benchmark.lua: -------------------------------------------------------------------------------- 1 | 2 | unique_thread_id = tostring( {} ):sub(8) 3 | counter = 0 4 | request = function() 5 | counter = counter + 1 6 | filter_name = "" .. unique_thread_id .. counter 7 | path = "/v1/bloomfilter/" .. filter_name 8 | wrk.headers["content-type"] = "application/json; charset=utf-8" 9 | return wrk.format("PUT", path, nil, "{\"expectedInsertions\": 10, \"fpp\": 0.1, \"validPeriod\": 5}") 10 | end 11 | -------------------------------------------------------------------------------- /filter-service-core/scripts/filter_service_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | create_filter() { 4 | local name=$1 5 | local expected_insertions=$2 6 | local fpp=$3 7 | local valid_period=${4:-0} 8 | 9 | curl -s -XPUT \ 10 | -H 'content-type: application/json; charset=utf-8' \ 11 | "http://localhost:8080/v1/bloomfilter/$name" \ 12 | -d@- > /dev/null < /dev/null < /dev/null 39 | 40 | echo "Filter: \"$name\" deleted." 41 | } 42 | 43 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/AbstractBloomFilterConfig.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkNotNull; 4 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkParameter; 5 | 6 | public abstract class AbstractBloomFilterConfig> implements BloomFilterConfig, Cloneable { 7 | private int expectedInsertions; 8 | private double fpp; 9 | 10 | AbstractBloomFilterConfig() { 11 | this(Configuration.defaultExpectedInsertions(), Configuration.defaultFalsePositiveProbability()); 12 | } 13 | 14 | AbstractBloomFilterConfig(int expectedInsertions, double fpp) { 15 | this.expectedInsertions = expectedInsertions; 16 | this.fpp = fpp; 17 | } 18 | 19 | public int expectedInsertions() { 20 | checkNotNull("expectedInsertions", expectedInsertions); 21 | return expectedInsertions; 22 | } 23 | 24 | public double fpp() { 25 | checkNotNull("fpp", fpp); 26 | return fpp; 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | @Override 31 | public T setExpectedInsertions(int expectedInsertions) { 32 | checkParameter("expectedInsertions", 33 | expectedInsertions > 0, 34 | "expected: > 0, actual: %s", 35 | expectedInsertions); 36 | 37 | this.expectedInsertions = expectedInsertions; 38 | return self(); 39 | } 40 | 41 | @SuppressWarnings("unchecked") 42 | @Override 43 | public T setFpp(double fpp) { 44 | checkParameter("fpp", 45 | fpp > 0.0d && fpp < 1.0d, 46 | "expected: > 0 && < 1, actual: %s", 47 | fpp); 48 | 49 | this.fpp = fpp; 50 | return self(); 51 | } 52 | 53 | @Override 54 | public boolean equals(Object o) { 55 | if (this == o) return true; 56 | if (o == null || getClass() != o.getClass()) return false; 57 | final AbstractBloomFilterConfig that = (AbstractBloomFilterConfig) o; 58 | return expectedInsertions() == that.expectedInsertions() && 59 | Double.compare(that.fpp(), fpp()) == 0; 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | int ret = Integer.hashCode(expectedInsertions); 65 | ret = 31 * ret + Double.hashCode(fpp); 66 | return ret; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "expectedInsertions=" + expectedInsertions + 72 | ", fpp=" + fpp; 73 | } 74 | 75 | protected abstract T self(); 76 | } 77 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BackgroundJobScheduler.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import io.micrometer.core.instrument.MeterRegistry; 4 | import io.micrometer.core.instrument.Timer; 5 | 6 | import java.time.Duration; 7 | import java.util.concurrent.*; 8 | 9 | final class BackgroundJobScheduler { 10 | private final ScheduledExecutorService scheduledExecutorService; 11 | private final MeterRegistry registry; 12 | private final CopyOnWriteArrayList> futures; 13 | 14 | BackgroundJobScheduler(MeterRegistry registry, ScheduledExecutorService scheduledExecutorService) { 15 | this.registry = registry; 16 | this.futures = new CopyOnWriteArrayList<>(); 17 | this.scheduledExecutorService = scheduledExecutorService; 18 | } 19 | 20 | void scheduleFixedIntervalJob(Runnable runnable, String name, Duration interval) { 21 | final Timer timer = registry.timer(Configuration.metricsPrefix() + "." + name); 22 | final ScheduledFuture future = scheduledExecutorService.scheduleWithFixedDelay( 23 | timer.wrap(runnable), 24 | interval.toMillis(), 25 | interval.toMillis(), 26 | TimeUnit.MILLISECONDS); 27 | futures.add(future); 28 | } 29 | 30 | void stop() { 31 | for (ScheduledFuture f : futures) { 32 | f.cancel(false); 33 | } 34 | 35 | futures.clear(); 36 | final CompletableFuture shutdownFuture = new CompletableFuture<>(); 37 | scheduledExecutorService.execute(() -> 38 | shutdownFuture.complete(null) 39 | ); 40 | 41 | shutdownFuture.join(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BadParameterException.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * A {@link IllegalArgumentException} that is raised when the parameter for an API 5 | * do not meet the contract. 6 | */ 7 | final class BadParameterException extends IllegalArgumentException { 8 | private static final long serialVersionUID = -1L; 9 | 10 | /** 11 | * Create a {@code BadParameterException} with the name of an invalid parameter. 12 | * 13 | * @param paramName the name of the invalid parameter. 14 | * @return the created {@code BadParameterException} 15 | */ 16 | static BadParameterException invalidParameter(String paramName) { 17 | return new BadParameterException(String.format("invalid parameter: \"%s\"", paramName)); 18 | } 19 | 20 | /** 21 | * Create a {@code BadParameterException} with the name of an invalid parameter and an 22 | * error message about why this parameter is invalid. 23 | * 24 | * @param paramName the name of the invalid parameter. 25 | * @param errorMsg the error message about why the input parameter is invalid 26 | * @return the created {@code BadParameterException} 27 | */ 28 | static BadParameterException invalidParameter(String paramName, String errorMsg) { 29 | return new BadParameterException(String.format("invalid parameter: \"%s\". %s", paramName, errorMsg)); 30 | } 31 | 32 | /** 33 | * Create a {@code BadParameterException} with the name of a required parameter. This 34 | * is used when a API require a parameter but it's not provided. 35 | * 36 | * @param paramName the name of the invalid parameter. 37 | * @return the created {@code BadParameterException} 38 | */ 39 | static BadParameterException requiredParameter(String paramName) { 40 | return new BadParameterException(String.format("required parameter: \"%s\"", paramName)); 41 | } 42 | 43 | private BadParameterException(String errorMsg) { 44 | super(errorMsg); 45 | } 46 | 47 | // We don't need stack trace for this exception 48 | @Override 49 | public synchronized Throwable fillInStackTrace() { 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilter.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | /** 7 | * An abstract bloom filter interface used to decouple our service 8 | * with the actual bloom filter implementations. 9 | */ 10 | public interface BloomFilter { 11 | /** 12 | * Get the number of expected insertions to this {@code BloomFilter}. 13 | * 14 | * @return the number of expected insertions 15 | */ 16 | int expectedInsertions(); 17 | 18 | /** 19 | * Get the desired false positive probability of this {@code BloomFilter}. 20 | * 21 | * @return the desired false positive probability 22 | */ 23 | double fpp(); 24 | 25 | /** 26 | * Returns {@code true} if the input {@code value} might have been 27 | * put in this Bloom filter before, {@code false} if this is 28 | * definitely not the case. 29 | * 30 | * @param value the testing value 31 | * @return true if the {@code value} might have been put in this 32 | * filter, false if this is definitely not the case. 33 | */ 34 | boolean mightContain(String value); 35 | 36 | /** 37 | * Puts an element into this {@code BloomFilter}. Ensures that subsequent invocations of {@link 38 | * #mightContain(String)} with the same element will always return {@code true}. 39 | * 40 | * @param value the value to put into this {@code BloomFilter} 41 | * @return true if the Bloom filter's bits changed as a result of this operation. If the bits 42 | * changed, this is definitely the first time {@code object} has been added to the 43 | * filter. If the bits haven't changed, this might be the first time {@code value} has 44 | * been added to the filter. Note that {@code set(String)} always returns the opposite 45 | * result to what {@code mightContain(String)} would have returned at the time it is called. 46 | */ 47 | boolean set(String value); 48 | 49 | /** 50 | * Check if this {@code BloomFilter} is still valid. Only valid {@code BloomFilter} can stay 51 | * in this service. Otherwise, it should be cleaned in an appropriate time. 52 | * 53 | * @return if this {@code BloomFilter} is valid 54 | */ 55 | boolean valid(); 56 | 57 | /** 58 | * Serialize this {@code BloomFilter} to a {@link OutputStream}. 59 | * 60 | * @param out the {@link OutputStream} to write to 61 | * @throws IOException if an I/O error occurs. 62 | */ 63 | void writeTo(OutputStream out) throws IOException; 64 | } 65 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterConfig.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * Basic configurations for a {@link BloomFilter}. 5 | * 6 | * @param The actual subtype of this {@link BloomFilterConfig} 7 | */ 8 | public interface BloomFilterConfig> { 9 | /** 10 | * Get the number of expected insertions to a {@link BloomFilter}. 11 | * 12 | * @return the configured number of expected insertions 13 | */ 14 | int expectedInsertions(); 15 | 16 | /** 17 | * Set the number of expected insertions to a {@link BloomFilter}. 18 | * 19 | * @param expectedInsertions the number of expected insertions (must be positive) 20 | * @return this 21 | */ 22 | T setExpectedInsertions(int expectedInsertions); 23 | 24 | /** 25 | * Get the desired false positive probability for a {@link BloomFilter}. 26 | * 27 | * @return the desired false positive probability. 28 | */ 29 | double fpp(); 30 | 31 | /** 32 | * Set the desired false positive probability for a {@link BloomFilter}. 33 | * 34 | * @param fpp the desired false positive probability (must be positive and less than 1.0). 35 | * @return this 36 | */ 37 | T setFpp(double fpp); 38 | } 39 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterFactory.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * A factory used to create a {@link BloomFilter}. 8 | * 9 | * @param the type of the created {@link BloomFilter} 10 | * @param the type of {@link BloomFilterConfig} used to create {@link BloomFilter} 11 | */ 12 | public interface BloomFilterFactory { 13 | /** 14 | * Create a {@link BloomFilter} with type T. 15 | * 16 | * @param config the configuration used to create {@link BloomFilter} 17 | * @return the created {@link BloomFilter} 18 | */ 19 | F createFilter(C config); 20 | 21 | /** 22 | * Deserialize a {@link BloomFilter} with type T from a {@link InputStream}. 23 | * 24 | * @param stream the {@link InputStream} to read from 25 | * @return a {@link BloomFilter} deserialized from the bytes read from the {@code InputStream} 26 | * @throws IOException if any I/O error occurs 27 | */ 28 | F readFrom(InputStream stream) throws IOException; 29 | } 30 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterHttpService.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.BloomFilterManager.CreateFilterResult; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.node.ArrayNode; 7 | import com.fasterxml.jackson.databind.node.BooleanNode; 8 | import com.linecorp.armeria.common.HttpResponse; 9 | import com.linecorp.armeria.common.HttpStatus; 10 | import com.linecorp.armeria.common.MediaType; 11 | import com.linecorp.armeria.server.annotation.*; 12 | 13 | import java.time.Duration; 14 | 15 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkNotNull; 16 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkParameter; 17 | 18 | /** 19 | * An http service powered by armeria to expose RESTFul APIs for Bloom filter operations. 20 | */ 21 | @ExceptionHandler(GlobalExceptionHandler.class) 22 | public final class BloomFilterHttpService { 23 | private static final ObjectMapper MAPPER = new ObjectMapper(); 24 | 25 | private final BloomFilterManager bloomFilterManager; 26 | 27 | public BloomFilterHttpService(BloomFilterManager bloomFilterManager) { 28 | this.bloomFilterManager = bloomFilterManager; 29 | } 30 | 31 | @Put("/{name}") 32 | public HttpResponse create(@Param String name, 33 | @RequestObject JsonNode req) { 34 | final JsonNode expectedInsertions = req.get("expectedInsertions"); 35 | final JsonNode fpp = req.get("fpp"); 36 | final JsonNode validPeriodAfterCreate = req.get("validPeriod") == null ? 37 | req.get("validPeriodAfterCreate") : req.get("validPeriod"); 38 | final JsonNode validPeriodAfterAccess = req.get("validPeriodAfterAccess"); 39 | final JsonNode overwrite = req.get("overwrite"); 40 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 41 | 42 | if (expectedInsertions != null) { 43 | config.setExpectedInsertions(expectedInsertions.intValue()); 44 | } 45 | if (fpp != null) { 46 | config.setFpp(fpp.doubleValue()); 47 | } 48 | 49 | if (validPeriodAfterCreate != null) { 50 | config.setValidPeriodAfterCreate(Duration.ofSeconds(validPeriodAfterCreate.intValue())); 51 | } 52 | 53 | if (validPeriodAfterAccess != null) { 54 | config.setValidPeriodAfterAccess(Duration.ofSeconds(validPeriodAfterAccess.intValue())); 55 | } 56 | 57 | final CreateFilterResult createResult; 58 | if (overwrite != null && overwrite.isBoolean() && overwrite.asBoolean()) { 59 | createResult = bloomFilterManager.createFilter(name, config, true); 60 | } else { 61 | createResult = bloomFilterManager.createFilter(name, config); 62 | } 63 | return HttpResponse.of( 64 | createResult.isCreated() ? HttpStatus.CREATED : HttpStatus.OK, 65 | MediaType.JSON_UTF_8, 66 | MAPPER.valueToTree(createResult.getFilter()).toString()); 67 | } 68 | 69 | @Get("/{name}") 70 | public JsonNode getFilterInfo(@Param String name) throws FilterNotFoundException { 71 | final BloomFilter filter = bloomFilterManager.ensureGetValidFilter(name); 72 | return MAPPER.valueToTree(filter); 73 | } 74 | 75 | @Get("/list") 76 | public JsonNode list() { 77 | final ArrayNode response = MAPPER.createArrayNode(); 78 | 79 | for (final String name : bloomFilterManager.getAllFilterNames()) { 80 | response.add(name); 81 | } 82 | 83 | return response; 84 | } 85 | 86 | @Post("/{name}/check") 87 | public JsonNode check(@Param String name, 88 | @RequestObject JsonNode req) 89 | throws FilterNotFoundException { 90 | final JsonNode testingValue = checkNotNull("value", req.get("value")); 91 | checkParameter("value", testingValue.isTextual(), "expect string type"); 92 | 93 | final BloomFilter filter = bloomFilterManager.ensureGetValidFilter(name); 94 | final boolean contain = filter.mightContain(testingValue.textValue()); 95 | return BooleanNode.valueOf(contain); 96 | } 97 | 98 | @Post("/{name}/multi-check") 99 | public JsonNode multiCheck(@Param String name, 100 | @RequestObject JsonNode req) 101 | throws FilterNotFoundException { 102 | final JsonNode values = checkNotNull("values", req.get("values")); 103 | checkParameter("values", values.isArray(), "expect Json array"); 104 | 105 | final BloomFilter filter = bloomFilterManager.ensureGetValidFilter(name); 106 | final ArrayNode response = MAPPER.createArrayNode(); 107 | for (final JsonNode value : values) { 108 | response.add(value.isTextual() && filter.mightContain(value.textValue())); 109 | } 110 | return response; 111 | } 112 | 113 | @Post("/{name}/check-and-set") 114 | public JsonNode checkAndSet(@Param String name, 115 | @RequestObject JsonNode req) 116 | throws FilterNotFoundException { 117 | final JsonNode testingValue = checkNotNull("value", req.get("value")); 118 | checkParameter("value", testingValue.isTextual(), "expect string type"); 119 | 120 | final BloomFilter filter = bloomFilterManager.ensureGetValidFilter(name); 121 | final boolean contain = !filter.set(testingValue.textValue()); 122 | return BooleanNode.valueOf(contain); 123 | } 124 | 125 | @Post("/{name}/multi-check-and-set") 126 | public JsonNode multiCheckAndSet(@Param String name, 127 | @RequestObject JsonNode req) 128 | throws FilterNotFoundException { 129 | final JsonNode values = checkNotNull("values", req.get("values")); 130 | checkParameter("values", values.isArray(), "expect Json array"); 131 | 132 | final BloomFilter filter = bloomFilterManager.ensureGetValidFilter(name); 133 | final ArrayNode response = MAPPER.createArrayNode(); 134 | for (final JsonNode value : values) { 135 | if (value.isTextual()) { 136 | response.add(BooleanNode.valueOf(!filter.set(value.textValue()))); 137 | } else { 138 | response.add(BooleanNode.FALSE); 139 | } 140 | } 141 | return response; 142 | } 143 | 144 | @Delete("/{name}") 145 | public HttpResponse remove(@Param String name) { 146 | bloomFilterManager.remove(name); 147 | return HttpResponse.of(HttpStatus.OK); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterManager.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.List; 5 | 6 | /** 7 | * A manager to manage {@link BloomFilter}s. 8 | * 9 | * @param the type of the managed {@link BloomFilter}s 10 | * @param the type of the configuration used by the managed {@link BloomFilter}s 11 | */ 12 | public interface BloomFilterManager> extends Iterable> { 13 | /** 14 | * The result of the create filter operations. 15 | * 16 | * @param same with the type F on {@link BloomFilterManager} 17 | */ 18 | final class CreateFilterResult { 19 | private final F filter; 20 | private final boolean created; 21 | 22 | /** 23 | * Create a result. 24 | * 25 | * @param filter The newly created filter when {@code created} is true 26 | * or the previous exists filter if {@code created} is false. 27 | * @param created true when {@code filter} is newly created, 28 | * or false when {@code filter} is an exists filter 29 | */ 30 | public CreateFilterResult(F filter, boolean created) { 31 | this.filter = filter; 32 | this.created = created; 33 | } 34 | 35 | /** 36 | * Get the contained {@code filter}. 37 | * 38 | * @return the contained {@code filter}. 39 | */ 40 | public F getFilter() { 41 | return filter; 42 | } 43 | 44 | /** 45 | * Check if the contained {@code filter} is newly created. 46 | * 47 | * @return true when the {@code filter} is newly created 48 | */ 49 | public boolean isCreated() { 50 | return created; 51 | } 52 | } 53 | 54 | /** 55 | * Create a new Bloom filter with the input {@code name} and {@code configuration} only when 56 | * there's no Bloom filter with the same {@code name} exists in this {@code BloomFilterManager}. 57 | * 58 | * @param name the name of the Bloom filter to create 59 | * @param config the configuration used to create a new Bloom filter 60 | * @return a {@link CreateFilterResult} instance contains a newly created filter if no Bloom filter 61 | * with the same {@code name} exists or the previous exists Bloom filter with the same 62 | * {@code name} 63 | */ 64 | default CreateFilterResult createFilter(String name, C config) { 65 | return createFilter(name, config, false); 66 | } 67 | 68 | /** 69 | * Try to create a new Bloom filter with the input configuration. 70 | * 71 | * @param name the name of the Bloom filter to create 72 | * @param config the configuration used to create a new Bloom filter 73 | * @param overwrite true to create a new Bloom filter and put it into this {@code BloomFilterManager} 74 | * no matter whether there's already an Bloom filter with the same {@code name} exits. 75 | * false have the same effect with {@link #createFilter(String, BloomFilterConfig)} 76 | * @return a {@link CreateFilterResult} instance contains a newly created filter if no Bloom filter 77 | * with the same {@code name} exists or {@code overwrite} is true, otherwise contains the 78 | * previous exists Bloom filter with the same {@code name} 79 | */ 80 | CreateFilterResult createFilter(String name, C config, boolean overwrite); 81 | 82 | /** 83 | * Add existent Bloom filters to this manager. 84 | * 85 | * @param filters a {@link Iterable} of the filters to add 86 | */ 87 | void addFilters(Iterable> filters); 88 | 89 | /** 90 | * Get the Bloom filter with the input name including the invalid one. 91 | * 92 | * @param name the name of the target Bloom filter 93 | * @return the Bloom filter if it exists, or null if no Bloom filter with 94 | * the target name in this manager 95 | */ 96 | @Nullable 97 | F getFilter(String name); 98 | 99 | /** 100 | * Get the Bloom filter with the input name. The difference between this method 101 | * and {@link #getFilter(String)} is that this method will throw a {@link FilterNotFoundException} 102 | * instead of returns the filter when it is invalid or null when there's no Bloom filter with 103 | * the target name in this manager. 104 | * 105 | * @param name the name of the target Bloom filter 106 | * @return the Bloom filter with the target name in this manager 107 | * @throws FilterNotFoundException when there's no Bloom filter with the target name 108 | * exists in this manager 109 | */ 110 | F ensureGetValidFilter(String name) throws FilterNotFoundException; 111 | 112 | /** 113 | * Returns all the names of the Bloom filters in this manager including the invalid ones. 114 | * 115 | * @return a list of names of the Bloom filters in this manager. 116 | */ 117 | List getAllFilterNames(); 118 | 119 | /** 120 | * Returns how many Bloom filters managed by this manager. 121 | * 122 | * @return how many Bloom filters managed by this manager. 123 | */ 124 | int size(); 125 | 126 | /** 127 | * Remove a Bloom filter with target name from this manager. 128 | * 129 | * @param name the name of the Bloom filter to remove. 130 | */ 131 | void remove(String name); 132 | 133 | /** 134 | * Remove a Bloom filter with target name from this manager only 135 | * if the target name currently mapped to a given filter. 136 | * 137 | * @param name the name of the Bloom filter to remove. 138 | * @param filter the Bloom filter to remove. 139 | */ 140 | void remove(String name, F filter); 141 | } 142 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterManagerImpl.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.CopyOnWriteArrayList; 9 | import java.util.concurrent.locks.Lock; 10 | import java.util.concurrent.locks.ReentrantLock; 11 | import java.util.function.Consumer; 12 | 13 | public final class BloomFilterManagerImpl> 14 | implements BloomFilterManager, 15 | Listenable> { 16 | private static final FilterNotFoundException FILTER_NOT_FOUND_EXCEPTION = new FilterNotFoundException(); 17 | 18 | private final List> listeners; 19 | private final ConcurrentHashMap filterMap; 20 | private final BloomFilterFactory factory; 21 | private final Lock filterMapLock = new ReentrantLock(); 22 | 23 | BloomFilterManagerImpl(BloomFilterFactory factory) { 24 | this.filterMap = new ConcurrentHashMap<>(); 25 | this.factory = factory; 26 | this.listeners = new CopyOnWriteArrayList<>(); 27 | } 28 | 29 | @Override 30 | public CreateFilterResult createFilter(String name, C config, boolean overwrite) { 31 | T filter; 32 | T prevFilter; 33 | boolean created = false; 34 | filterMapLock.lock(); 35 | try { 36 | prevFilter = filterMap.get(name); 37 | if (overwrite || prevFilter == null || !prevFilter.valid()) { 38 | filter = factory.createFilter(config); 39 | filterMap.put(name, filter); 40 | created = true; 41 | } else { 42 | filter = prevFilter; 43 | } 44 | } finally { 45 | filterMapLock.unlock(); 46 | } 47 | 48 | if (created) { 49 | if (prevFilter != null) { 50 | notifyBloomFilterRemoved(name, prevFilter); 51 | } 52 | 53 | notifyBloomFilterCreated(name, config, filter); 54 | } 55 | 56 | return new CreateFilterResult<>(filter, created); 57 | } 58 | 59 | @Override 60 | public void addFilters(Iterable> filters) { 61 | filterMapLock.lock(); 62 | try { 63 | for (FilterRecord holder : filters) { 64 | filterMap.put(holder.name(), holder.filter()); 65 | } 66 | } finally { 67 | filterMapLock.unlock(); 68 | } 69 | } 70 | 71 | @Nullable 72 | @Override 73 | public T getFilter(String name) { 74 | return filterMap.get(name); 75 | } 76 | 77 | @Override 78 | public T ensureGetValidFilter(String name) throws FilterNotFoundException { 79 | final T filter = getFilter(name); 80 | if (filter == null || !filter.valid()) { 81 | throw FILTER_NOT_FOUND_EXCEPTION; 82 | } 83 | return filter; 84 | } 85 | 86 | @Override 87 | public List getAllFilterNames() { 88 | return new ArrayList<>(filterMap.keySet()); 89 | } 90 | 91 | @Override 92 | public int size() { 93 | return filterMap.size(); 94 | } 95 | 96 | @Override 97 | public void remove(String name) { 98 | final T filter; 99 | 100 | filterMapLock.lock(); 101 | try { 102 | filter = filterMap.remove(name); 103 | } finally { 104 | filterMapLock.unlock(); 105 | } 106 | 107 | if (filter != null) { 108 | notifyBloomFilterRemoved(name, filter); 109 | } 110 | } 111 | 112 | @Override 113 | public void remove(String name, T filter) { 114 | final boolean removed; 115 | 116 | filterMapLock.lock(); 117 | try { 118 | removed = filterMap.remove(name, filter); 119 | } finally { 120 | filterMapLock.unlock(); 121 | } 122 | 123 | if (removed) { 124 | notifyBloomFilterRemoved(name, filter); 125 | } 126 | } 127 | 128 | @Override 129 | public Iterator> iterator() { 130 | return filterMap.entrySet().stream().map(e -> new FilterRecord<>(e.getKey(), e.getValue())).iterator(); 131 | } 132 | 133 | @Override 134 | public void addListener(BloomFilterManagerListener listener) { 135 | listeners.add(listener); 136 | } 137 | 138 | @Override 139 | public boolean removeListener(BloomFilterManagerListener listener) { 140 | return listeners.remove(listener); 141 | } 142 | 143 | private void notifyBloomFilterCreated(String name, C config, T filter) { 144 | notifyListeners(l -> l.onBloomFilterCreated(name, config, filter)); 145 | } 146 | 147 | private void notifyBloomFilterRemoved(String name, T filter) { 148 | notifyListeners(l -> l.onBloomFilterRemoved(name, filter)); 149 | } 150 | 151 | private void notifyListeners(Consumer> consumer) { 152 | for (BloomFilterManagerListener listener : listeners) { 153 | consumer.accept(listener); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/BloomFilterManagerListener.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * A lister for {@link BloomFilterManager}. 5 | * 6 | * @param the type of the Bloom filters managed by {@link BloomFilterManager} 7 | * @param the type of the configuration used by {@link BloomFilterManager} 8 | */ 9 | public interface BloomFilterManagerListener> { 10 | /** 11 | * Called when a Bloom filter was created by {@link BloomFilterManager}. 12 | * Please note do not block in this method, due to this maybe a synchronous method. 13 | * 14 | * @param name the name of the operated filter 15 | * @param config the configuration used to create the {@code filter} 16 | * @param filter the newly created Bloom filter 17 | */ 18 | default void onBloomFilterCreated(String name, C config, F filter) {} 19 | 20 | /** 21 | * Called when a Bloom filter was removed from {@link BloomFilterManager}. 22 | * Please note do not block in this method, due to this maybe a synchronous method. 23 | * 24 | * @param name the name of the operated filter 25 | * @param filter the removed Bloom filter 26 | */ 27 | default void onBloomFilterRemoved(String name, F filter) {} 28 | } 29 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/CountUpdateBloomFilterFactory.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.concurrent.atomic.LongAdder; 6 | 7 | public final class CountUpdateBloomFilterFactory> 8 | implements BloomFilterFactory { 9 | private final BloomFilterFactory factory; 10 | private final LongAdder filterUpdateTimesCounter; 11 | 12 | CountUpdateBloomFilterFactory(BloomFilterFactory factory, LongAdder filterUpdateTimesCounter) { 13 | this.factory = factory; 14 | this.filterUpdateTimesCounter = filterUpdateTimesCounter; 15 | } 16 | 17 | LongAdder filterUpdateTimesCounter() { 18 | return filterUpdateTimesCounter; 19 | } 20 | 21 | @Override 22 | public CountUpdateBloomFilterWrapper createFilter(C config) { 23 | filterUpdateTimesCounter.increment(); 24 | return new CountUpdateBloomFilterWrapper(factory.createFilter(config), filterUpdateTimesCounter); 25 | } 26 | 27 | @Override 28 | public CountUpdateBloomFilterWrapper readFrom(InputStream stream) throws IOException { 29 | return new CountUpdateBloomFilterWrapper(factory.readFrom(stream), filterUpdateTimesCounter); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/CountUpdateBloomFilterWrapper.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.util.concurrent.atomic.LongAdder; 8 | 9 | public final class CountUpdateBloomFilterWrapper implements BloomFilter { 10 | private final LongAdder filterUpdateTimesCounter; 11 | @JsonUnwrapped 12 | private final BloomFilter filter; 13 | 14 | CountUpdateBloomFilterWrapper(BloomFilter filter, LongAdder filterUpdateTimesCounter) { 15 | this.filterUpdateTimesCounter = filterUpdateTimesCounter; 16 | this.filter = filter; 17 | } 18 | 19 | @Override 20 | public int expectedInsertions() { 21 | return filter.expectedInsertions(); 22 | } 23 | 24 | @Override 25 | public double fpp() { 26 | return filter.fpp(); 27 | } 28 | 29 | @Override 30 | public boolean mightContain(String value) { 31 | return filter.mightContain(value); 32 | } 33 | 34 | @Override 35 | public boolean set(String value) { 36 | filterUpdateTimesCounter.increment(); 37 | return filter.set(value); 38 | } 39 | 40 | @Override 41 | public boolean valid() { 42 | return filter.valid(); 43 | } 44 | 45 | @Override 46 | public void writeTo(OutputStream out) throws IOException { 47 | filter.writeTo(out); 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | final CountUpdateBloomFilterWrapper wrapper = (CountUpdateBloomFilterWrapper) o; 55 | return filterUpdateTimesCounter.equals(wrapper.filterUpdateTimesCounter) && 56 | filter.equals(wrapper.filter); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return filter.hashCode(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/DefaultMetricsService.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.metrics.MetricsService; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import io.micrometer.core.instrument.logging.LoggingMeterRegistry; 6 | 7 | /** 8 | * A default implementation of {@link MeterRegistry}. It is used when no SPI service 9 | * which implement {@link MeterRegistry} can be found under path {@code META-INF/services}. 10 | * It will write all the metrics to log. 11 | */ 12 | public final class DefaultMetricsService implements MetricsService { 13 | @Override 14 | public MeterRegistry createMeterRegistry() { 15 | return new LoggingMeterRegistry(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/Errors.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | 7 | public enum Errors { 8 | UNKNOWN_ERROR(-1, "unknown error"), 9 | NONE(0, "none"), 10 | BAD_PARAMETER(1, "invalid parameter"), 11 | FILTER_NOT_FOUND(2, "filter not found"); 12 | 13 | private static final ObjectMapper MAPPER = new ObjectMapper(); 14 | 15 | private final short code; 16 | private final String defaultErrorMsg; 17 | 18 | Errors(int code, String defaultErrorMsg) { 19 | this.code = (short) code; 20 | this.defaultErrorMsg = defaultErrorMsg; 21 | } 22 | 23 | public JsonNode buildErrorInfoInJson() { 24 | return buildErrorInfoInJson(defaultErrorMsg); 25 | } 26 | 27 | public JsonNode buildErrorInfoInJson(String errorMsg) { 28 | final ObjectNode error = MAPPER.createObjectNode(); 29 | error.put("error", errorMsg); 30 | error.put("code", code); 31 | return error; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/ExpirableBloomFilter.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.time.ZonedDateTime; 4 | 5 | /** 6 | * A bloom filter which have expiration and can expire after expiration. 7 | */ 8 | public interface ExpirableBloomFilter extends BloomFilter { 9 | /** 10 | * The time of this Bloom filter will expire. 11 | * 12 | * @return the expiration time of this Bloom filter 13 | */ 14 | ZonedDateTime expiration(); 15 | 16 | /** 17 | * Check if this Bloom filter is already expired. 18 | * 19 | * @return true when this Bloom filter is already expired. 20 | */ 21 | boolean expired(); 22 | 23 | @Override 24 | default boolean valid() { 25 | return !expired(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/ExpirableBloomFilterConfig.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import javax.annotation.Nullable; 4 | import java.time.Duration; 5 | import java.time.ZonedDateTime; 6 | 7 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkNotNull; 8 | import static cn.leancloud.filter.service.ServiceParameterPreconditions.checkParameter; 9 | 10 | final class ExpirableBloomFilterConfig extends AbstractBloomFilterConfig { 11 | private Duration validPeriodAfterCreate; 12 | @Nullable 13 | private Duration validPeriodAfterAccess; 14 | 15 | ExpirableBloomFilterConfig() { 16 | this.validPeriodAfterCreate = Configuration.defaultValidPeriodAfterCreate(); 17 | } 18 | 19 | ExpirableBloomFilterConfig(int expectedInsertions, double fpp) { 20 | super(expectedInsertions, fpp); 21 | this.validPeriodAfterCreate = Configuration.defaultValidPeriodAfterCreate(); 22 | } 23 | 24 | ExpirableBloomFilterConfig(ExpirableBloomFilterConfig config) { 25 | super(config.expectedInsertions(), config.fpp()); 26 | this.validPeriodAfterAccess = config.validPeriodAfterAccess(); 27 | } 28 | 29 | Duration validPeriodAfterCreate() { 30 | return validPeriodAfterCreate; 31 | } 32 | 33 | ExpirableBloomFilterConfig setValidPeriodAfterCreate(Duration validPeriod) { 34 | checkNotNull("validPeriodAfterCreate", validPeriod); 35 | checkParameter("validPeriodAfterCreate", 36 | validPeriod.getSeconds() > 0L, 37 | "expected: > 0, actual: %s", 38 | validPeriod); 39 | 40 | this.validPeriodAfterCreate = validPeriod; 41 | return this; 42 | } 43 | 44 | @Nullable 45 | Duration validPeriodAfterAccess() { 46 | return validPeriodAfterAccess; 47 | } 48 | 49 | ExpirableBloomFilterConfig setValidPeriodAfterAccess(Duration validPeriod) { 50 | checkNotNull("validPeriodAfterAccess", validPeriod); 51 | checkParameter("validPeriodAfterAccess", 52 | validPeriod.getSeconds() > 0L, 53 | "expected: > 0, actual: %s", 54 | validPeriod); 55 | 56 | this.validPeriodAfterAccess = validPeriod; 57 | 58 | return self(); 59 | } 60 | 61 | /** 62 | * Compute the expiration time of the {@link BloomFilter}. 63 | * 64 | * @param creation the creation time of a {@link BloomFilter} 65 | * @return the expiration time for {@link BloomFilter} 66 | */ 67 | ZonedDateTime expiration(ZonedDateTime creation) { 68 | ZonedDateTime expiration; 69 | if (validPeriodAfterAccess != null) { 70 | expiration = creation.plus(validPeriodAfterAccess); 71 | } else { 72 | expiration = creation.plus(validPeriodAfterCreate); 73 | } 74 | return expiration; 75 | } 76 | 77 | @Override 78 | public boolean equals(Object o) { 79 | if (this == o) return true; 80 | if (o == null || getClass() != o.getClass()) return false; 81 | if (!super.equals(o)) return false; 82 | final ExpirableBloomFilterConfig that = (ExpirableBloomFilterConfig) o; 83 | return validPeriodAfterCreate.equals(that.validPeriodAfterCreate) && 84 | (validPeriodAfterAccess == null || validPeriodAfterAccess.equals(that.validPeriodAfterAccess)); 85 | } 86 | 87 | @Override 88 | public int hashCode() { 89 | int ret = super.hashCode(); 90 | ret = 31 * ret + validPeriodAfterCreate.hashCode(); 91 | 92 | if (validPeriodAfterAccess != null) { 93 | ret = 31 * ret + validPeriodAfterAccess.hashCode(); 94 | } 95 | return ret; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "ExpirableBloomFilterConfig{" + 101 | super.toString() + 102 | "validPeriodAfterCreate=" + validPeriodAfterCreate + 103 | ", validPeriodAfterAccess=" + validPeriodAfterAccess + 104 | '}'; 105 | } 106 | 107 | @Override 108 | protected ExpirableBloomFilterConfig self() { 109 | return this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/FilterNotFoundException.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * A {@link Exception} that is raised when a requested filter did not found. 5 | */ 6 | public final class FilterNotFoundException extends Exception { 7 | private static final long serialVersionUID = -1L; 8 | 9 | // We don't need stack trace for this exception 10 | @Override 11 | public synchronized Throwable fillInStackTrace() { 12 | return this; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/FilterRecord.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.utils.ChecksumedBufferedOutputStream; 4 | 5 | import java.io.*; 6 | import java.nio.ByteBuffer; 7 | import java.nio.channels.Channels; 8 | import java.nio.channels.FileChannel; 9 | import java.nio.channels.GatheringByteChannel; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * The schema is: 14 | * BodyLength: Int32 15 | * MAGIC: Byte 16 | * CRC: Uint32 17 | * NameLength: Int32 18 | * Name: Bytes 19 | * Filter: Bytes 20 | */ 21 | public final class FilterRecord { 22 | static final int BODY_LENGTH_OFFSET = 0; 23 | static final int BODY_LENGTH_LENGTH = 4; 24 | static final int MAGIC_OFFSET = BODY_LENGTH_OFFSET + BODY_LENGTH_LENGTH; 25 | static final int MAGIC_LENGTH = 1; 26 | static final int CRC_OFFSET = MAGIC_OFFSET + MAGIC_LENGTH; 27 | static final int CRC_LENGTH = 4; 28 | static final int HEADER_OVERHEAD = CRC_OFFSET + CRC_LENGTH; 29 | 30 | static final byte DEFAULT_MAGIC = (byte) 0; 31 | 32 | private final String name; 33 | private final F filter; 34 | 35 | public FilterRecord(String name, F filter) { 36 | this.name = name; 37 | this.filter = filter; 38 | } 39 | 40 | public String name() { 41 | return name; 42 | } 43 | 44 | public F filter() { 45 | return filter; 46 | } 47 | 48 | public int writeFullyTo(FileChannel channel) throws IOException { 49 | final long startPos = channel.position(); 50 | // write body first then we can know how large the body is 51 | channel.position(startPos + HEADER_OVERHEAD); 52 | 53 | // we don't need to close this stream. it'll be effectively closed when the underlying channel closed 54 | final ChecksumedBufferedOutputStream stream = new ChecksumedBufferedOutputStream( 55 | Channels.newOutputStream(channel), 56 | Configuration.channelBufferSizeForFilterPersistence()); 57 | writeBody(stream); 58 | stream.flush(); 59 | 60 | // write header 61 | int bodyLen = (int) (channel.position() - startPos - HEADER_OVERHEAD); 62 | channel.position(startPos); 63 | 64 | final ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_OVERHEAD); 65 | headerBuffer.putInt(BODY_LENGTH_OFFSET, bodyLen); 66 | headerBuffer.put(MAGIC_OFFSET, DEFAULT_MAGIC); 67 | headerBuffer.putInt(CRC_OFFSET, (int) stream.checksum()); 68 | writeBufferTo(channel, headerBuffer); 69 | 70 | // move position forward to the end of this record 71 | channel.position(startPos + HEADER_OVERHEAD + bodyLen); 72 | return HEADER_OVERHEAD + bodyLen; 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) return true; 78 | if (o == null || getClass() != o.getClass()) return false; 79 | final FilterRecord that = (FilterRecord) o; 80 | return name.equals(that.name) && 81 | filter.equals(that.filter); 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | int ret = name.hashCode(); 87 | ret = 31 * ret + filter.hashCode(); 88 | return ret; 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return "FilterRecord{" + 94 | "name='" + name + '\'' + 95 | ", filter=" + filter + 96 | '}'; 97 | } 98 | 99 | private void writeBody(OutputStream out) throws IOException { 100 | final DataOutputStream dout = new DataOutputStream(out); 101 | final byte[] nameInBytes = name.getBytes(StandardCharsets.UTF_8); 102 | dout.writeInt(nameInBytes.length); 103 | dout.write(nameInBytes); 104 | filter.writeTo(out); 105 | } 106 | 107 | private int writeBufferTo(GatheringByteChannel channel, ByteBuffer buffer) throws IOException { 108 | buffer.mark(); 109 | int written = 0; 110 | while (written < buffer.limit()) { 111 | written = channel.write(buffer); 112 | } 113 | buffer.reset(); 114 | return written; 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/FilterRecordInputStream.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.utils.Crc32C; 4 | 5 | import javax.annotation.Nullable; 6 | import java.io.ByteArrayInputStream; 7 | import java.io.Closeable; 8 | import java.io.EOFException; 9 | import java.io.IOException; 10 | import java.nio.ByteBuffer; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.charset.StandardCharsets; 13 | import java.nio.file.Path; 14 | import java.nio.file.StandardOpenOption; 15 | 16 | import static cn.leancloud.filter.service.FilterRecord.*; 17 | import static cn.leancloud.filter.service.UnfinishedFilterException.shortReadFilterBody; 18 | import static cn.leancloud.filter.service.UnfinishedFilterException.shortReadFilterHeader; 19 | 20 | public final class FilterRecordInputStream implements Closeable { 21 | private static void readFully(FileChannel channel, ByteBuffer destinationBuffer, long position) throws IOException { 22 | if (position < 0) { 23 | throw new IllegalArgumentException("position:" + position + " (expected: >=0)"); 24 | } 25 | long currentPosition = position; 26 | int bytesRead; 27 | do { 28 | bytesRead = channel.read(destinationBuffer, currentPosition); 29 | currentPosition += bytesRead; 30 | } while (bytesRead != -1 && destinationBuffer.hasRemaining()); 31 | } 32 | 33 | static void readFullyOrFail(FileChannel channel, ByteBuffer destinationBuffer, long position) throws IOException { 34 | if (position < 0) { 35 | throw new IllegalArgumentException("position:" + position + " (expected: >=0)"); 36 | } 37 | final int expectedReadBytes = destinationBuffer.remaining(); 38 | readFully(channel, destinationBuffer, position); 39 | if (destinationBuffer.hasRemaining()) { 40 | throw new EOFException(String.format("Failed to read `FilterRecord` from file channel `%s`. Expected to read %d bytes, " + 41 | "but reached end of file after reading %d bytes. Started read from position %d.", 42 | channel, expectedReadBytes, expectedReadBytes - destinationBuffer.remaining(), position)); 43 | } 44 | } 45 | 46 | private final FileChannel channel; 47 | private final Path recordFilePath; 48 | private final long end; 49 | private final ByteBuffer headerBuffer; 50 | private final BloomFilterFactory factory; 51 | private long position; 52 | 53 | public FilterRecordInputStream(Path recordFilePath, BloomFilterFactory factory) throws IOException { 54 | this.recordFilePath = recordFilePath; 55 | this.channel = FileChannel.open(recordFilePath, StandardOpenOption.READ); 56 | this.position = channel.position(); 57 | this.headerBuffer = ByteBuffer.allocate(HEADER_OVERHEAD); 58 | this.factory = factory; 59 | this.end = channel.size(); 60 | } 61 | 62 | @Nullable 63 | public FilterRecord nextFilterRecord() throws IOException { 64 | if (end - position <= HEADER_OVERHEAD) { 65 | if (end == position) { 66 | return null; 67 | } 68 | 69 | throw shortReadFilterHeader(recordFilePath.toString(), (int) (HEADER_OVERHEAD - (end - position))); 70 | } 71 | 72 | headerBuffer.rewind(); 73 | readFullyOrFail(channel, headerBuffer, position); 74 | headerBuffer.rewind(); 75 | 76 | final int bodyLen = headerBuffer.getInt(BODY_LENGTH_OFFSET); 77 | if (end - position < HEADER_OVERHEAD + bodyLen) { 78 | throw shortReadFilterBody(recordFilePath.toString(), (int) ((bodyLen + HEADER_OVERHEAD) - (end - position))); 79 | } 80 | 81 | checkMagic(headerBuffer); 82 | 83 | final ByteBuffer bodyBuffer = ByteBuffer.allocate(bodyLen); 84 | readFullyOrFail(channel, bodyBuffer, position + HEADER_OVERHEAD); 85 | bodyBuffer.flip(); 86 | 87 | checkCrc(headerBuffer, bodyBuffer); 88 | 89 | final ByteBuffer filterNameBuffer = readFilterNameBuffer(bodyBuffer); 90 | final String name = StandardCharsets.UTF_8.decode(filterNameBuffer).toString(); 91 | final F filter = factory.readFrom(new ByteArrayInputStream(bodyBuffer.array(), 92 | bodyBuffer.position() + filterNameBuffer.limit(), bodyBuffer.remaining())); 93 | 94 | // every thing is fine, we move position forward 95 | position += bodyLen + HEADER_OVERHEAD; 96 | return new FilterRecord<>(name, filter); 97 | } 98 | 99 | @Override 100 | public void close() throws IOException { 101 | channel.close(); 102 | } 103 | 104 | private void checkMagic(ByteBuffer headerBuffer) { 105 | final byte magic = headerBuffer.get(MAGIC_OFFSET); 106 | if (magic != DEFAULT_MAGIC) { 107 | throw new InvalidFilterException("read unknown Magic: " + magic + " from position: " 108 | + position + " from file: " + recordFilePath); 109 | } 110 | } 111 | 112 | private void checkCrc(ByteBuffer headerBuffer, ByteBuffer bodyBuffer) { 113 | final long expectCrc = readCrc(headerBuffer); 114 | final long actualCrc = Crc32C.compute(bodyBuffer, 0, bodyBuffer.limit()); 115 | if (actualCrc != expectCrc) { 116 | throw new InvalidFilterException("got unmatched crc when read filter from position: " 117 | + position + " from file: " + recordFilePath + ". expect: " + expectCrc + ", actual: " + actualCrc); 118 | } 119 | } 120 | 121 | private ByteBuffer readFilterNameBuffer(ByteBuffer bodyBuffer) { 122 | final int nameLength = bodyBuffer.getInt(); 123 | final ByteBuffer filterNameBuffer = bodyBuffer.slice(); 124 | filterNameBuffer.limit(nameLength); 125 | return filterNameBuffer; 126 | } 127 | 128 | private long readCrc(ByteBuffer headerBuffer) { 129 | // read unsigned int 130 | return headerBuffer.getInt(CRC_OFFSET) & 0xffffffffL; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/FilterServiceFileUtils.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.annotation.Nullable; 7 | import java.io.IOException; 8 | import java.nio.channels.Channel; 9 | import java.nio.channels.FileChannel; 10 | import java.nio.channels.FileLock; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.StandardCopyOption; 14 | import java.nio.file.StandardOpenOption; 15 | 16 | final class FilterServiceFileUtils { 17 | private static final Logger logger = LoggerFactory.getLogger(FilterServiceFileUtils.class); 18 | 19 | static FileLock lockDirectory(Path baseDir, String lockFileName) throws IOException { 20 | final Path lockFilePath = baseDir.resolve(lockFileName); 21 | final FileChannel lockChannel = FileChannel.open(lockFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE); 22 | FileLock fileLock = null; 23 | try { 24 | fileLock = lockChannel.tryLock(); 25 | if (fileLock == null) { 26 | throw new IllegalStateException("failed to lock directory: " + baseDir); 27 | } 28 | } finally { 29 | if (fileLock == null || !fileLock.isValid()) { 30 | lockChannel.close(); 31 | } 32 | } 33 | 34 | return fileLock; 35 | } 36 | 37 | static void releaseDirectoryLock(@Nullable FileLock lock) throws IOException { 38 | if (lock != null) { 39 | try (Channel channel = lock.acquiredBy()) { 40 | if (channel.isOpen()) { 41 | lock.release(); 42 | } 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Attempts to move source to target atomically and falls back to a non-atomic move if it fails. 49 | * 50 | * @throws IOException if both atomic and non-atomic moves fail 51 | */ 52 | static void atomicMoveWithFallback(Path source, Path target) throws IOException { 53 | try { 54 | Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); 55 | } catch (IOException outer) { 56 | try { 57 | Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); 58 | logger.debug("Non-atomic move of {} to {} succeeded after atomic move failed due to {}", source, target, 59 | outer.getMessage()); 60 | } catch (IOException inner) { 61 | inner.addSuppressed(outer); 62 | throw inner; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.linecorp.armeria.common.HttpRequest; 4 | import com.linecorp.armeria.common.HttpResponse; 5 | import com.linecorp.armeria.common.HttpStatus; 6 | import com.linecorp.armeria.common.MediaType; 7 | import com.linecorp.armeria.server.ServiceRequestContext; 8 | import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public final class GlobalExceptionHandler implements ExceptionHandlerFunction { 13 | private static final Logger logger = LoggerFactory.getLogger(BloomFilterHttpService.class); 14 | 15 | @Override 16 | public HttpResponse handleException(ServiceRequestContext ctx, HttpRequest req, Throwable cause) { 17 | if (cause instanceof FilterNotFoundException) { 18 | return HttpResponse.of(HttpStatus.NOT_FOUND, 19 | MediaType.JSON_UTF_8, 20 | Errors.FILTER_NOT_FOUND.buildErrorInfoInJson().toString()); 21 | } else if (cause instanceof BadParameterException) { 22 | return HttpResponse.of(HttpStatus.BAD_REQUEST, 23 | MediaType.JSON_UTF_8, 24 | Errors.BAD_PARAMETER.buildErrorInfoInJson(cause.getMessage()).toString()); 25 | } else if (cause instanceof IllegalArgumentException) { 26 | return HttpResponse.of(HttpStatus.BAD_REQUEST, 27 | MediaType.JSON_UTF_8, 28 | Errors.BAD_PARAMETER.buildErrorInfoInJson().toString()); 29 | } 30 | 31 | logger.error("Got unexpected exception on req {}", req, cause); 32 | return HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/GuavaBloomFilterFactory.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.time.ZoneOffset; 6 | import java.time.ZonedDateTime; 7 | 8 | public class GuavaBloomFilterFactory implements BloomFilterFactory { 9 | @Override 10 | public GuavaBloomFilter createFilter(ExpirableBloomFilterConfig config) { 11 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 12 | final ZonedDateTime expiration = config.expiration(creation); 13 | 14 | return new GuavaBloomFilter( 15 | config.expectedInsertions(), 16 | config.fpp(), 17 | creation, 18 | expiration, 19 | config.validPeriodAfterAccess()); 20 | } 21 | 22 | @Override 23 | public GuavaBloomFilter readFrom(InputStream stream) throws IOException { 24 | return GuavaBloomFilter.readFrom(stream); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/InvalidBloomFilterPurgatory.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | public final class InvalidBloomFilterPurgatory implements Purgatory { 4 | private BloomFilterManager manager; 5 | 6 | public InvalidBloomFilterPurgatory(BloomFilterManager manager) { 7 | this.manager = manager; 8 | } 9 | 10 | @Override 11 | public void purge() { 12 | for (FilterRecord holder : manager) { 13 | final F filter = holder.filter(); 14 | if (!filter.valid()) { 15 | final String name = holder.name(); 16 | manager.remove(name, filter); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/InvalidFilterException.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | public class InvalidFilterException extends PersistentStorageException { 4 | private static final long serialVersionUID = -1L; 5 | 6 | public InvalidFilterException() { 7 | } 8 | 9 | public InvalidFilterException(String message) { 10 | super(message); 11 | } 12 | 13 | public InvalidFilterException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public InvalidFilterException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/Listenable.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * The subtype of this interface can be listened by a listener of type L. 5 | * 6 | * @param the type of the listener 7 | */ 8 | public interface Listenable { 9 | void addListener(L listener); 10 | 11 | boolean removeListener(L listener); 12 | } 13 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/NonNullByDefault.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import javax.annotation.meta.TypeQualifierDefault; 6 | import java.lang.annotation.*; 7 | 8 | /** 9 | * An annotation that signifies the return values, parameters and fields are non-nullable by default 10 | * leveraging the JSR-305 {@link Nonnull} annotation. Annotate a package with this annotation and 11 | * annotate nullable return values, parameters and fields with {@link Nullable}. 12 | */ 13 | @Nonnull 14 | @Documented 15 | @Target(ElementType.PACKAGE) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @TypeQualifierDefault({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) 18 | public @interface NonNullByDefault { 19 | } 20 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/PersistentFiltersJob.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.Configuration.TriggerPersistenceCriteria; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.atomic.LongAdder; 9 | 10 | public class PersistentFiltersJob implements Runnable { 11 | private static final Logger logger = LoggerFactory.getLogger(PersistentFiltersJob.class); 12 | 13 | private final BloomFilterManager bloomFilterManager; 14 | private final PersistentManager persistentManager; 15 | private final LongAdder filterUpdateTimesCounter; 16 | private final TriggerPersistenceCriteria criteria; 17 | 18 | PersistentFiltersJob(BloomFilterManager bloomFilterManager, 19 | PersistentManager persistentManager, 20 | LongAdder filterUpdateTimesCounter, 21 | TriggerPersistenceCriteria criteria) { 22 | this.bloomFilterManager = bloomFilterManager; 23 | this.persistentManager = persistentManager; 24 | this.filterUpdateTimesCounter = filterUpdateTimesCounter; 25 | this.criteria = criteria; 26 | } 27 | 28 | @Override 29 | public void run() { 30 | // synchronized on an instance of a class only to prevent several PersistentFiltersJob to 31 | // access on the same filterUpdateTimesCounter. It's not mean to and can't prevent thread 32 | // not in PersistentFiltersJob to access this counter 33 | synchronized (filterUpdateTimesCounter) { 34 | final long sum = filterUpdateTimesCounter.sum(); 35 | if (sum > criteria.updatesThreshold()) { 36 | if (logger.isDebugEnabled()) { 37 | logger.debug("Updated {} times in last {} seconds meets threshold {} to persistence filters", 38 | sum, criteria.checkingPeriod().getSeconds(), criteria.updatesThreshold()); 39 | } 40 | 41 | filterUpdateTimesCounter.reset(); 42 | doPersistence(); 43 | } 44 | } 45 | } 46 | 47 | private void doPersistence() { 48 | try { 49 | persistentManager.freezeAllFilters(bloomFilterManager); 50 | } catch (IOException ex) { 51 | logger.error("Persistent bloom filters failed.", ex); 52 | } catch (Throwable t) { 53 | // sorry for the duplication, but currently I don't figure out another way 54 | // to catch the direct buffer OOM when freeze filters to file 55 | logger.error("Persistent bloom filters failed.", t); 56 | throw t; 57 | } 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/PersistentManager.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.utils.AbstractIterator; 4 | import org.apache.commons.io.FileUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.annotation.Nullable; 9 | import java.io.Closeable; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.channels.FileChannel; 13 | import java.nio.channels.FileLock; 14 | import java.nio.file.Path; 15 | import java.nio.file.StandardOpenOption; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public class PersistentManager implements Closeable { 20 | private static final Logger logger = LoggerFactory.getLogger(PersistentManager.class); 21 | private static final String LOCK_FILE_NAME = "lock"; 22 | private static final String TEMPORARY_PERSISTENT_FILE_SUFFIX = ".tmp"; 23 | private static final String PERSISTENT_FILE_SUFFIX = ".db"; 24 | private static final String PERSISTENT_FILE_NAME = "snapshot"; 25 | 26 | private final Path basePath; 27 | private final FileLock fileLock; 28 | 29 | PersistentManager(Path persistentPath) 30 | throws IOException { 31 | final File dir = persistentPath.toFile(); 32 | if (dir.exists() && !dir.isDirectory()) { 33 | throw new IllegalStateException("invalid persistent directory path, it's a regular file: " + persistentPath); 34 | } 35 | 36 | FileUtils.forceMkdir(persistentPath.toFile()); 37 | 38 | this.fileLock = FilterServiceFileUtils.lockDirectory(persistentPath, LOCK_FILE_NAME); 39 | this.basePath = persistentPath; 40 | } 41 | 42 | synchronized void freezeAllFilters(Iterable> records) throws IOException { 43 | final Path tempPath = temporaryPersistentFilePath(); 44 | int counter = 0; 45 | try (FileChannel channel = FileChannel.open(tempPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) { 46 | for (FilterRecord record : records) { 47 | record.writeFullyTo(channel); 48 | counter++; 49 | } 50 | 51 | channel.force(true); 52 | } 53 | 54 | FilterServiceFileUtils.atomicMoveWithFallback(tempPath, persistentFilePath()); 55 | logger.debug("Persistent " + counter + " filters."); 56 | } 57 | 58 | synchronized List> recoverFilters(BloomFilterFactory factory, 59 | boolean allowRecoverFromCorruptedFile) 60 | throws IOException { 61 | final List> records = new ArrayList<>(); 62 | if (persistentFilePath().toFile().exists()) { 63 | records.addAll(recoverFiltersFromFile(factory, allowRecoverFromCorruptedFile, persistentFilePath())); 64 | } 65 | 66 | if (temporaryPersistentFilePath().toFile().exists()) { 67 | try { 68 | records.addAll(recoverFiltersFromFile(factory, allowRecoverFromCorruptedFile, temporaryPersistentFilePath())); 69 | } catch (IOException | PersistentStorageException ex) { 70 | logger.warn("Failed to recover from the file: \"{}\" and got error msg: \"{}\". We just ignore it and carry on.", 71 | temporaryPersistentFilePath(), ex.getMessage()); 72 | } 73 | } 74 | 75 | return records; 76 | } 77 | 78 | @Override 79 | public synchronized void close() throws IOException { 80 | FilterServiceFileUtils.releaseDirectoryLock(fileLock); 81 | } 82 | 83 | // Package private for testing 84 | Path temporaryPersistentFilePath() { 85 | return basePath.resolve(PERSISTENT_FILE_NAME + TEMPORARY_PERSISTENT_FILE_SUFFIX); 86 | } 87 | 88 | // Package private for testing 89 | Path persistentFilePath() { 90 | return basePath.resolve(PERSISTENT_FILE_NAME + PERSISTENT_FILE_SUFFIX); 91 | } 92 | 93 | private List> recoverFiltersFromFile(BloomFilterFactory factory, 94 | boolean allowRecoverFromCorruptedFile, 95 | Path filePath) throws IOException { 96 | final List> records = new ArrayList<>(); 97 | try { 98 | try (FilterRecordInputStream filterStream = new FilterRecordInputStream<>(filePath, factory)) { 99 | readFiltersFromFile(filterStream) 100 | .forEach(r -> { 101 | if (r.filter().valid()) { 102 | records.add(r); 103 | } 104 | }); 105 | } 106 | logger.info("Recovered " + records.size() + " filters from: " + filePath); 107 | return records; 108 | } catch (InvalidFilterException ex) { 109 | if (allowRecoverFromCorruptedFile) { 110 | logger.warn("Recover " + records.size() + " filters from corrupted file:" + filePath + 111 | ". The exception captured as follows:", ex); 112 | return records; 113 | } else { 114 | throw new PersistentStorageException("failed to recover filters from: " + filePath, ex); 115 | } 116 | } 117 | } 118 | 119 | private Iterable> readFiltersFromFile(FilterRecordInputStream filterStream) { 120 | return () -> new AbstractIterator>() { 121 | @Nullable 122 | @Override 123 | protected FilterRecord makeNext() { 124 | try { 125 | final FilterRecord record = filterStream.nextFilterRecord(); 126 | if (record == null) { 127 | allDone(); 128 | } 129 | return record; 130 | } catch (IOException ex) { 131 | throw new InvalidFilterException("read filter from file:" + persistentFilePath() + " failed", ex); 132 | } 133 | } 134 | }; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/PersistentStorageException.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | public class PersistentStorageException extends RuntimeException { 4 | private static final long serialVersionUID = -1L; 5 | 6 | public PersistentStorageException() { 7 | } 8 | 9 | public PersistentStorageException(String message) { 10 | super(message); 11 | } 12 | 13 | public PersistentStorageException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public PersistentStorageException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/Purgatory.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | /** 4 | * A purgatory which have something to purge. 5 | */ 6 | public interface Purgatory { 7 | void purge(); 8 | } 9 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/PurgeFiltersJob.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class PurgeFiltersJob implements Runnable { 7 | private static final Logger logger = LoggerFactory.getLogger(PurgeFiltersJob.class); 8 | private final Purgatory purgatory; 9 | 10 | PurgeFiltersJob(Purgatory purgatory) { 11 | this.purgatory = purgatory; 12 | } 13 | 14 | @Override 15 | public void run() { 16 | try { 17 | purgatory.purge(); 18 | } catch (Exception ex) { 19 | logger.error("Purge bloom filter service failed.", ex); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/ServerOptions.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import picocli.CommandLine.Command; 4 | import picocli.CommandLine.Option; 5 | 6 | import javax.annotation.Nullable; 7 | 8 | @Command(name = "filter-service", 9 | sortOptions = false, 10 | showDefaultValues = true, 11 | version = "filter-service v1.14", 12 | description = "filter-service is a daemon network service which is used to expose bloom filters " + 13 | "and operations by RESTFul API.", 14 | mixinStandardHelpOptions = true) 15 | final class ServerOptions { 16 | @Option(names = {"-c", "--configuration-file"}, 17 | description = "The path to a YAML configuration file.") 18 | @Nullable 19 | private String configFilePath; 20 | @Option(names = {"-p", "--port"}, 21 | defaultValue = "8080", 22 | description = "The http/https port on which filter-service is running.") 23 | private int port; 24 | 25 | @Option(names = {"-d", "--enable-doc-service"}, 26 | defaultValue = "false", 27 | description = "true when you want to serve the testing document service under path \"/docs\".") 28 | private boolean docService; 29 | 30 | int port() { 31 | return port; 32 | } 33 | 34 | boolean docServiceEnabled() { 35 | return docService; 36 | } 37 | 38 | @Nullable 39 | String configFilePath() { 40 | return configFilePath; 41 | } 42 | 43 | ServerOptions() { 44 | 45 | } 46 | 47 | ServerOptions(@Nullable String configFilePath, int port, boolean docService) { 48 | this.configFilePath = configFilePath; 49 | this.port = port; 50 | this.docService = docService; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/ServiceParameterPreconditions.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import static com.google.common.base.Strings.lenientFormat; 6 | 7 | public final class ServiceParameterPreconditions { 8 | /** 9 | * Ensures the truth of an expression involving one or more parameters to the calling method. 10 | * 11 | * @param paramName parameter name 12 | * @param expression a boolean expression 13 | * @throws IllegalArgumentException if {@code expression} is false 14 | */ 15 | public static void checkParameter(String paramName, boolean expression) { 16 | if (!expression) { 17 | throw BadParameterException.invalidParameter(paramName); 18 | } 19 | } 20 | 21 | /** 22 | * Ensures the truth of an expression involving one or more parameters to the calling method. 23 | * 24 | * @param paramName parameter name 25 | * @param expression a boolean expression 26 | * @param errorMessage the exception message to use if the check fails; will be converted to a 27 | * string using {@link String#valueOf(Object)} 28 | * @throws IllegalArgumentException if {@code expression} is false 29 | */ 30 | public static void checkParameter(String paramName, boolean expression, @Nullable Object errorMessage) { 31 | if (!expression) { 32 | throw BadParameterException.invalidParameter(paramName, String.valueOf(errorMessage)); 33 | } 34 | } 35 | 36 | /** 37 | * Ensures the truth of an expression involving one or more parameters to the calling method. 38 | * 39 | * @param paramName parameter name 40 | * @param expression a boolean expression 41 | * @param errorMessageTemplate a template for the exception message should the check fail. The 42 | * message is formed by replacing each {@code %s} placeholder in the template with an 43 | * argument. These are matched by position - the first {@code %s} gets {@code 44 | * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in 45 | * square braces. Unmatched placeholders will be left as-is. 46 | * @param errorMessageArgs the arguments to be substituted into the message template. Arguments 47 | * are converted to strings using {@link String#valueOf(Object)}. 48 | * @throws IllegalArgumentException if {@code expression} is false 49 | */ 50 | public static void checkParameter( 51 | String paramName, 52 | boolean expression, 53 | @Nullable String errorMessageTemplate, 54 | @Nullable Object... errorMessageArgs) { 55 | if (!expression) { 56 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, errorMessageArgs)); 57 | } 58 | } 59 | 60 | /** 61 | * Ensures the truth of an expression involving one or more parameters to the calling method. 62 | * 63 | *

See {@link #checkParameter(String, boolean, String, Object...)} for details. 64 | */ 65 | public static void checkParameter(String paramName, boolean b, @Nullable String errorMessageTemplate, int p1) { 66 | if (!b) { 67 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, p1)); 68 | } 69 | } 70 | 71 | /** 72 | * Ensures the truth of an expression involving one or more parameters to the calling method. 73 | * 74 | *

See {@link #checkParameter(String, boolean, String, Object...)} for details. 75 | */ 76 | public static void checkParameter( 77 | String paramName, 78 | boolean b, @Nullable String errorMessageTemplate, @Nullable Object p1) { 79 | if (!b) { 80 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, p1)); 81 | } 82 | } 83 | 84 | public static T checkNotNull(String paramName, @Nullable T obj) { 85 | if (obj == null) { 86 | throw BadParameterException.requiredParameter(paramName); 87 | } 88 | 89 | return obj; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/UnfinishedFilterException.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | public class UnfinishedFilterException extends InvalidFilterException { 4 | private static final long serialVersionUID = -1L; 5 | 6 | public UnfinishedFilterException() { 7 | } 8 | 9 | public UnfinishedFilterException(String message) { 10 | super(message); 11 | } 12 | 13 | public UnfinishedFilterException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public UnfinishedFilterException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | public static UnfinishedFilterException shortReadFilterHeader(String location, int shortBytes) { 22 | return new UnfinishedFilterException("not enough bytes to read filter record header from: " + location + ". " + 23 | shortBytes + " bytes short."); 24 | } 25 | 26 | public static UnfinishedFilterException shortReadFilterBody(String location, int shortBytes) { 27 | return new UnfinishedFilterException("not enough bytes to read filter record body from: " + location + ". " + 28 | shortBytes + " bytes short."); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/package-info.java: -------------------------------------------------------------------------------- 1 | 2 | 3 | @NonNullByDefault 4 | package cn.leancloud.filter.service; -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/AbstractIterator.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Iterator; 5 | import java.util.NoSuchElementException; 6 | 7 | /** 8 | * A base class that simplifies implementing an iterator 9 | * @param The type of thing we are iterating over 10 | */ 11 | public abstract class AbstractIterator implements Iterator { 12 | private enum State { 13 | READY, NOT_READY, DONE, FAILED 14 | } 15 | 16 | private State state = State.NOT_READY; 17 | @Nullable 18 | private T next; 19 | 20 | @Override 21 | public boolean hasNext() { 22 | switch (state) { 23 | case FAILED: 24 | throw new IllegalStateException("iterator is in failed state"); 25 | case DONE: 26 | return false; 27 | case READY: 28 | return true; 29 | default: 30 | return maybeComputeNext(); 31 | } 32 | } 33 | 34 | @Override 35 | public T next() { 36 | if (!hasNext()) 37 | throw new NoSuchElementException(); 38 | state = State.NOT_READY; 39 | if (next == null) 40 | throw new IllegalStateException("expected item but none found"); 41 | return next; 42 | } 43 | 44 | @Override 45 | public void remove() { 46 | throw new UnsupportedOperationException("removal not supported"); 47 | } 48 | 49 | public T peek() { 50 | if (!hasNext()) 51 | throw new NoSuchElementException(); 52 | assert next != null; 53 | return next; 54 | } 55 | 56 | @Nullable 57 | protected T allDone() { 58 | state = State.DONE; 59 | return null; 60 | } 61 | 62 | @Nullable 63 | protected abstract T makeNext(); 64 | 65 | private Boolean maybeComputeNext() { 66 | state = State.FAILED; 67 | next = makeNext(); 68 | if (state == State.DONE) { 69 | return false; 70 | } else { 71 | state = State.READY; 72 | return true; 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/ChecksumedBufferedOutputStream.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.util.zip.Checksum; 7 | 8 | public final class ChecksumedBufferedOutputStream extends FilterOutputStream { 9 | /** 10 | * The internal buffer where data is stored. 11 | */ 12 | private final byte[] buf; 13 | 14 | private final Checksum crc; 15 | 16 | /** 17 | * The number of valid bytes in the buffer. This value is always 18 | * in the range {@code 0} through {@code buf.length}; elements 19 | * {@code buf[0]} through {@code buf[count-1]} contain valid 20 | * byte data. 21 | */ 22 | private int count; 23 | 24 | /** 25 | * Creates a new buffered output stream to write data to the 26 | * specified underlying output stream. 27 | * 28 | * @param out the underlying output stream. 29 | */ 30 | public ChecksumedBufferedOutputStream(OutputStream out) { 31 | this(out, 8192); 32 | } 33 | 34 | /** 35 | * Creates a new buffered output stream to write data to the 36 | * specified underlying output stream with the specified buffer 37 | * size. 38 | * 39 | * @param out the underlying output stream. 40 | * @param size the buffer size. 41 | * @exception IllegalArgumentException if size <= 0. 42 | */ 43 | public ChecksumedBufferedOutputStream(OutputStream out, int size) { 44 | super(out); 45 | if (size <= 0) { 46 | throw new IllegalArgumentException("Buffer size <= 0"); 47 | } 48 | buf = new byte[size]; 49 | crc = Crc32C.create(); 50 | } 51 | 52 | /** Flush the internal buffer */ 53 | private void flushBuffer() throws IOException { 54 | if (count > 0) { 55 | out.write(buf, 0, count); 56 | crc.update(buf, 0, count); 57 | count = 0; 58 | } 59 | } 60 | 61 | /** 62 | * Writes the specified byte to this buffered output stream. 63 | * 64 | * @param b the byte to be written. 65 | * @exception IOException if an I/O error occurs. 66 | */ 67 | @Override 68 | public synchronized void write(int b) throws IOException { 69 | if (count >= buf.length) { 70 | flushBuffer(); 71 | } 72 | buf[count++] = (byte)b; 73 | } 74 | 75 | /** 76 | * Writes len bytes from the specified byte array 77 | * starting at offset off to this buffered output stream. 78 | * 79 | *

Ordinarily this method stores bytes from the given array into this 80 | * stream's buffer, flushing the buffer to the underlying output stream as 81 | * needed. If the requested length is at least as large as this stream's 82 | * buffer, however, then this method will flush the buffer and write the 83 | * bytes directly to the underlying output stream. Thus redundant 84 | * BufferedOutputStreams will not copy data unnecessarily. 85 | * 86 | * @param b the data. 87 | * @param off the start offset in the data. 88 | * @param len the number of bytes to write. 89 | * @exception IOException if an I/O error occurs. 90 | */ 91 | @Override 92 | public synchronized void write(byte[] b, int off, int len) throws IOException { 93 | if (len >= buf.length) { 94 | /* If the request length exceeds the size of the output buffer, 95 | flush the output buffer and then write the data directly. 96 | In this way buffered streams will cascade harmlessly. */ 97 | flushBuffer(); 98 | out.write(b, off, len); 99 | crc.update(b, off, len); 100 | return; 101 | } 102 | if (len > buf.length - count) { 103 | flushBuffer(); 104 | } 105 | System.arraycopy(b, off, buf, count, len); 106 | count += len; 107 | } 108 | 109 | /** 110 | * Flushes this buffered output stream. This forces any buffered 111 | * output bytes to be written out to the underlying output stream. 112 | * 113 | * @exception IOException if an I/O error occurs. 114 | * @see java.io.FilterOutputStream#out 115 | */ 116 | @Override 117 | public synchronized void flush() throws IOException { 118 | flushBuffer(); 119 | out.flush(); 120 | } 121 | 122 | public synchronized long checksum() { 123 | return crc.getValue(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Crc32C.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodType; 6 | import java.nio.ByteBuffer; 7 | import java.util.zip.Checksum; 8 | 9 | /** 10 | * A class that can be used to compute the CRC32C (Castagnoli) of a ByteBuffer or array of bytes. 11 | * 12 | * We use java.util.zip.CRC32C (introduced in Java 9) if it is available and fallback to PureJavaCrc32C, otherwise. 13 | * java.util.zip.CRC32C is significantly faster on reasonably modern CPUs as it uses the CRC32 instruction introduced 14 | * in SSE4.2. 15 | * 16 | * NOTE: This class is copied from Kafka. 17 | */ 18 | public final class Crc32C { 19 | 20 | private static final ChecksumFactory CHECKSUM_FACTORY; 21 | 22 | static { 23 | if (Java.IS_JAVA9_COMPATIBLE) 24 | CHECKSUM_FACTORY = new Java9ChecksumFactory(); 25 | else 26 | CHECKSUM_FACTORY = new PureJavaChecksumFactory(); 27 | } 28 | 29 | private Crc32C() {} 30 | 31 | /** 32 | * Compute the CRC32C (Castagnoli) of the segment of the byte array given by the specified size and offset 33 | * 34 | * @param bytes The bytes to checksum 35 | * @param offset the offset at which to begin the checksum computation 36 | * @param size the number of bytes to checksum 37 | * @return The CRC32C 38 | */ 39 | public static long compute(byte[] bytes, int offset, int size) { 40 | Checksum crc = create(); 41 | crc.update(bytes, offset, size); 42 | return crc.getValue(); 43 | } 44 | 45 | /** 46 | * Compute the CRC32C (Castagnoli) of a byte buffer from a given offset (relative to the buffer's current position) 47 | * 48 | * @param buffer The buffer with the underlying data 49 | * @param offset The offset relative to the current position 50 | * @param size The number of bytes beginning from the offset to include 51 | * @return The CRC32C 52 | */ 53 | public static long compute(ByteBuffer buffer, int offset, int size) { 54 | Checksum crc = create(); 55 | updateChecksums(crc, buffer, offset, size); 56 | return crc.getValue(); 57 | } 58 | 59 | public static Checksum create() { 60 | return CHECKSUM_FACTORY.create(); 61 | } 62 | 63 | private static void updateChecksums(Checksum checksum, ByteBuffer buffer, int offset, int length) { 64 | if (buffer.hasArray()) { 65 | checksum.update(buffer.array(), buffer.position() + buffer.arrayOffset() + offset, length); 66 | } else { 67 | int start = buffer.position() + offset; 68 | for (int i = start; i < start + length; i++) 69 | checksum.update(buffer.get(i)); 70 | } 71 | } 72 | 73 | private interface ChecksumFactory { 74 | Checksum create(); 75 | } 76 | 77 | private static class Java9ChecksumFactory implements ChecksumFactory { 78 | private static final MethodHandle CONSTRUCTOR; 79 | 80 | static { 81 | try { 82 | Class cls = Class.forName("java.util.zip.CRC32C"); 83 | CONSTRUCTOR = MethodHandles.publicLookup().findConstructor(cls, MethodType.methodType(void.class)); 84 | } catch (ReflectiveOperationException e) { 85 | // Should never happen 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | 90 | @Override 91 | public Checksum create() { 92 | try { 93 | return (Checksum) CONSTRUCTOR.invoke(); 94 | } catch (Throwable throwable) { 95 | // Should never happen 96 | throw new RuntimeException(throwable); 97 | } 98 | } 99 | } 100 | 101 | private static class PureJavaChecksumFactory implements ChecksumFactory { 102 | @Override 103 | public Checksum create() { 104 | return new PureJavaCrc32C(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/DefaultTimer.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import java.time.ZoneOffset; 4 | import java.time.ZonedDateTime; 5 | 6 | public final class DefaultTimer implements Timer{ 7 | @Override 8 | public ZonedDateTime utcNow() { 9 | return ZonedDateTime.now(ZoneOffset.UTC); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Java.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import java.util.StringTokenizer; 4 | 5 | public final class Java { 6 | 7 | private Java() { } 8 | 9 | private static final Version VERSION = parseVersion(System.getProperty("java.specification.version")); 10 | 11 | // Package private for testing 12 | static Version parseVersion(String versionString) { 13 | final StringTokenizer st = new StringTokenizer(versionString, "."); 14 | int majorVersion = Integer.parseInt(st.nextToken()); 15 | int minorVersion; 16 | if (st.hasMoreTokens()) 17 | minorVersion = Integer.parseInt(st.nextToken()); 18 | else 19 | minorVersion = 0; 20 | return new Version(majorVersion, minorVersion); 21 | } 22 | 23 | // Having these as static final provides the best opportunity for compiler optimization 24 | public static final boolean IS_JAVA9_COMPATIBLE = VERSION.isJava9Compatible(); 25 | 26 | // Package private for testing 27 | static class Version { 28 | public final int majorVersion; 29 | public final int minorVersion; 30 | 31 | private Version(int majorVersion, int minorVersion) { 32 | this.majorVersion = majorVersion; 33 | this.minorVersion = minorVersion; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "Version(majorVersion=" + majorVersion + 39 | ", minorVersion=" + minorVersion + ")"; 40 | } 41 | 42 | // Package private for testing 43 | boolean isJava9Compatible() { 44 | return majorVersion >= 9; 45 | } 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Timer.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import java.time.ZonedDateTime; 4 | 5 | public interface Timer { 6 | Timer DEFAULT_TIMER = new DefaultTimer(); 7 | 8 | ZonedDateTime utcNow(); 9 | } 10 | -------------------------------------------------------------------------------- /filter-service-core/src/main/java/cn/leancloud/filter/service/utils/package-info.java: -------------------------------------------------------------------------------- 1 | 2 | 3 | @NonNullByDefault 4 | package cn.leancloud.filter.service.utils; 5 | 6 | import cn.leancloud.filter.service.NonNullByDefault; -------------------------------------------------------------------------------- /filter-service-core/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/AbstractBloomFilterConfigTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.ThreadLocalRandom; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 9 | 10 | public class AbstractBloomFilterConfigTest { 11 | @Test 12 | public void testGetAndSetExpectedInsertions() { 13 | final int expectedEInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 14 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig(); 15 | assertThat(config.expectedInsertions()).isEqualTo(Configuration.defaultExpectedInsertions()); 16 | assertThat(config.setExpectedInsertions(expectedEInsertions)).isSameAs(config); 17 | assertThat(config.expectedInsertions()).isEqualTo(expectedEInsertions); 18 | } 19 | 20 | @Test 21 | public void testGetAndSetFpp() { 22 | final double expectedFpp = ThreadLocalRandom.current().nextDouble(0.0001, 1); 23 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig(); 24 | assertThat(config.fpp()).isEqualTo(Configuration.defaultFalsePositiveProbability()); 25 | assertThat(config.setFpp(expectedFpp)).isSameAs(config); 26 | assertThat(config.fpp()).isEqualTo(expectedFpp); 27 | } 28 | 29 | @Test 30 | public void testGetAndSetInvalidExpectedInsertions() { 31 | final int invalidExpectedInsertions = -1 * Math.abs(ThreadLocalRandom.current().nextInt()); 32 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig(); 33 | 34 | assertThatThrownBy(() -> config.setExpectedInsertions(invalidExpectedInsertions)) 35 | .isInstanceOf(BadParameterException.class) 36 | .hasMessageContaining("invalid parameter"); 37 | } 38 | 39 | @Test 40 | public void testGetAndSetInvalidFpp() { 41 | final double invalidFpp = ThreadLocalRandom.current().nextDouble(1, Long.MAX_VALUE); 42 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig(); 43 | 44 | assertThatThrownBy(() -> config.setFpp(invalidFpp)) 45 | .isInstanceOf(BadParameterException.class) 46 | .hasMessageContaining("invalid parameter"); 47 | } 48 | 49 | @Test 50 | public void testEquals() { 51 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1); 52 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 53 | final TestingBloomFilterConfig filterA = new TestingBloomFilterConfig() 54 | .setFpp(fpp) 55 | .setExpectedInsertions(expectedInsertions); 56 | 57 | final TestingBloomFilterConfig filterB = new TestingBloomFilterConfig() 58 | .setFpp(fpp) 59 | .setExpectedInsertions(expectedInsertions); 60 | 61 | assertThat(filterA).isEqualTo(filterB); 62 | } 63 | 64 | @Test 65 | public void testHashCode() { 66 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1); 67 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 68 | final TestingBloomFilterConfig filterA = new TestingBloomFilterConfig() 69 | .setFpp(fpp) 70 | .setExpectedInsertions(expectedInsertions); 71 | 72 | final TestingBloomFilterConfig filterB = new TestingBloomFilterConfig() 73 | .setFpp(fpp) 74 | .setExpectedInsertions(expectedInsertions); 75 | 76 | assertThat(filterA.hashCode()).isEqualTo(filterB.hashCode()); 77 | } 78 | 79 | private static class TestingBloomFilterConfig extends AbstractBloomFilterConfig { 80 | @Override 81 | protected TestingBloomFilterConfig self() { 82 | return this; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/AdjustableTimer.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.utils.Timer; 4 | 5 | import java.time.ZoneOffset; 6 | import java.time.ZonedDateTime; 7 | 8 | public class AdjustableTimer implements Timer { 9 | private ZonedDateTime now; 10 | 11 | public AdjustableTimer() { 12 | this.now = ZonedDateTime.now(ZoneOffset.UTC); 13 | } 14 | 15 | public void setNow(ZonedDateTime now) { 16 | this.now = now; 17 | } 18 | 19 | @Override 20 | public ZonedDateTime utcNow() { 21 | return now; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/BackgroundJobSchedulerTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.linecorp.armeria.common.metric.NoopMeterRegistry; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import org.junit.After; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import java.time.Duration; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.ScheduledExecutorService; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | public class BackgroundJobSchedulerTest { 19 | private static final String jobName = "TestingJobName"; 20 | private ScheduledExecutorService scheduledExecutorService; 21 | private MeterRegistry registry; 22 | private BackgroundJobScheduler scheduler; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | scheduledExecutorService = Executors.newScheduledThreadPool(1); 27 | registry = NoopMeterRegistry.get(); 28 | scheduler = new BackgroundJobScheduler(registry, scheduledExecutorService); 29 | } 30 | 31 | @After 32 | public void tearDown() throws Exception { 33 | scheduler.stop(); 34 | scheduledExecutorService.shutdown(); 35 | } 36 | 37 | @Test 38 | public void testStopScheduler() throws Exception { 39 | final CountDownLatch executed = new CountDownLatch(1); 40 | final AtomicInteger executedTimes = new AtomicInteger(); 41 | final AtomicBoolean interrupt = new AtomicBoolean(false); 42 | scheduler.scheduleFixedIntervalJob(() -> { 43 | try { 44 | executed.countDown(); 45 | Thread.sleep(1000); 46 | executedTimes.incrementAndGet(); 47 | } catch (InterruptedException ex) { 48 | interrupt.set(true); 49 | } 50 | }, jobName, Duration.ofMillis(10)); 51 | 52 | executed.await(); 53 | scheduler.stop(); 54 | assertThat(executedTimes).hasValue(1); 55 | assertThat(interrupt).isFalse(); 56 | } 57 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/BootstrapTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.Bootstrap.ParseCommandLineArgsResult; 4 | import org.apache.commons.io.FileUtils; 5 | import org.junit.Test; 6 | import picocli.CommandLine.ExitCode; 7 | 8 | import java.nio.file.Paths; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class BootstrapTest { 13 | 14 | @Test 15 | public void testStartStop() throws Exception { 16 | final String configFilePath = "src/test/resources/testing-configuration.yaml"; 17 | final ServerOptions opts = new ServerOptions(configFilePath, 8080, false); 18 | final Bootstrap bootstrap = new Bootstrap(opts); 19 | bootstrap.start(true); 20 | 21 | bootstrap.stop(); 22 | FileUtils.forceDelete(Paths.get("lock").toFile()); 23 | } 24 | 25 | @Test 26 | public void testHelp() { 27 | final String[] args = new String[]{"-h"}; 28 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 29 | assertThat(ret.isExit()).isTrue(); 30 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 31 | assertThat(ret.getOptions()).isNull(); 32 | } 33 | 34 | @Test 35 | public void testHelp2() { 36 | final String[] args = new String[]{"--help"}; 37 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 38 | assertThat(ret.isExit()).isTrue(); 39 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 40 | assertThat(ret.getOptions()).isNull(); 41 | } 42 | 43 | @Test 44 | public void testVersion() { 45 | final String[] args = new String[]{"-V"}; 46 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 47 | assertThat(ret.isExit()).isTrue(); 48 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 49 | assertThat(ret.getOptions()).isNull(); 50 | } 51 | 52 | @Test 53 | public void testVersion2() { 54 | final String[] args = new String[]{"--version"}; 55 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 56 | assertThat(ret.isExit()).isTrue(); 57 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 58 | assertThat(ret.getOptions()).isNull(); 59 | } 60 | 61 | @Test 62 | public void testUnknownArgument() { 63 | final String[] args = new String[]{"-unknown"}; 64 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 65 | assertThat(ret.isExit()).isTrue(); 66 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE); 67 | assertThat(ret.getOptions()).isNull(); 68 | } 69 | 70 | @Test 71 | public void testInvalidPort() { 72 | String[] args = new String[]{"-p", "wahaha"}; 73 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 74 | assertThat(ret.isExit()).isTrue(); 75 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE); 76 | assertThat(ret.getOptions()).isNull(); 77 | } 78 | 79 | @Test 80 | public void testInvalidPort2() { 81 | String[] args = new String[]{"--port", "wahaha"}; 82 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 83 | assertThat(ret.isExit()).isTrue(); 84 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE); 85 | assertThat(ret.getOptions()).isNull(); 86 | } 87 | 88 | @Test 89 | public void testInvalidEnableDocService() { 90 | String[] args = new String[]{"-d", "wahaha"}; 91 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 92 | assertThat(ret.isExit()).isTrue(); 93 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE); 94 | assertThat(ret.getOptions()).isNull(); 95 | } 96 | 97 | @Test 98 | public void testArgsInAbbreviationForm() { 99 | String[] args = new String[]{"-d", "-c", "path/to/config", "-p", "8080"}; 100 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 101 | assertThat(ret.isExit()).isFalse(); 102 | 103 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 104 | ServerOptions options = ret.getOptions(); 105 | assertThat(options).isNotNull(); 106 | assertThat(options.docServiceEnabled()).isTrue(); 107 | assertThat(options.port()).isEqualTo(8080); 108 | assertThat(options.configFilePath()).isEqualTo("path/to/config"); 109 | } 110 | 111 | @Test 112 | public void testArgsInFullForm() { 113 | String[] args = new String[]{"--enable-doc-service", "--configuration-file", "path/to/config", "--port", "8080"}; 114 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args); 115 | assertThat(ret.isExit()).isFalse(); 116 | 117 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK); 118 | ServerOptions options = ret.getOptions(); 119 | assertThat(options).isNotNull(); 120 | assertThat(options.docServiceEnabled()).isTrue(); 121 | assertThat(options.port()).isEqualTo(8080); 122 | assertThat(options.configFilePath()).isEqualTo("path/to/config"); 123 | } 124 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/CountUpdateBloomFilterFactoryTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.concurrent.atomic.LongAdder; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 13 | import static org.mockito.Mockito.*; 14 | 15 | @SuppressWarnings("unchecked") 16 | public class CountUpdateBloomFilterFactoryTest { 17 | private LongAdder filterUpdateTimesCounter; 18 | private BloomFilterFactory innerFilterFactory; 19 | private CountUpdateBloomFilterFactory wrapper; 20 | 21 | @Before 22 | public void setUp() { 23 | filterUpdateTimesCounter = new LongAdder(); 24 | innerFilterFactory = mock(BloomFilterFactory.class); 25 | wrapper = new CountUpdateBloomFilterFactory(innerFilterFactory, filterUpdateTimesCounter); 26 | } 27 | 28 | @Test 29 | public void testDelegate() throws Exception { 30 | final InputStream in = new ByteArrayInputStream(new byte[0]); 31 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 32 | final BloomFilter filter = mock(BloomFilter.class); 33 | when(innerFilterFactory.createFilter(config)).thenReturn(filter); 34 | when(innerFilterFactory.readFrom(in)).thenReturn(filter); 35 | 36 | assertThat(wrapper.createFilter(config)).isEqualTo(new CountUpdateBloomFilterWrapper(filter, filterUpdateTimesCounter)); 37 | assertThat(wrapper.readFrom(in)).isEqualTo(new CountUpdateBloomFilterWrapper(filter, filterUpdateTimesCounter)); 38 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1); 39 | 40 | verify(innerFilterFactory, times(1)).createFilter(config); 41 | verify(innerFilterFactory, times(1)).readFrom(in); 42 | } 43 | 44 | @Test 45 | public void testReadFromThrowsException() throws Exception { 46 | final InputStream in = new ByteArrayInputStream(new byte[0]); 47 | final IOException expectedEx = new IOException("expected exception"); 48 | 49 | doThrow(expectedEx).when(innerFilterFactory).readFrom(in); 50 | 51 | assertThatThrownBy(() -> wrapper.readFrom(in)).isEqualTo(expectedEx); 52 | verify(innerFilterFactory, times(1)).readFrom(in); 53 | } 54 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/CountUpdateBloomFilterWrapperTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.time.Duration; 11 | import java.util.concurrent.atomic.LongAdder; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | import static org.mockito.Mockito.*; 16 | 17 | public class CountUpdateBloomFilterWrapperTest { 18 | private LongAdder filterUpdateTimesCounter; 19 | private BloomFilter innerFilter; 20 | private CountUpdateBloomFilterWrapper wrapper; 21 | 22 | @Before 23 | public void setUp() { 24 | filterUpdateTimesCounter = new LongAdder(); 25 | innerFilter = mock(BloomFilter.class); 26 | wrapper = new CountUpdateBloomFilterWrapper(innerFilter, filterUpdateTimesCounter); 27 | } 28 | 29 | @Test 30 | public void testDelegate() throws IOException { 31 | final int expectedInsertions = 1000; 32 | final double fpp = 0.01; 33 | final String testingValue = "testingValue"; 34 | final OutputStream out = new ByteArrayOutputStream(); 35 | 36 | when(innerFilter.expectedInsertions()).thenReturn(expectedInsertions); 37 | when(innerFilter.fpp()).thenReturn(fpp); 38 | when(innerFilter.mightContain(testingValue)).thenReturn(true); 39 | when(innerFilter.set(testingValue)).thenReturn(false); 40 | when(innerFilter.valid()).thenReturn(false); 41 | doNothing().when(innerFilter).writeTo(out); 42 | 43 | assertThat(wrapper.expectedInsertions()).isEqualTo(expectedInsertions); 44 | assertThat(wrapper.fpp()).isEqualTo(fpp); 45 | assertThat(wrapper.mightContain(testingValue)).isTrue(); 46 | assertThat(wrapper.set(testingValue)).isFalse(); 47 | assertThat(wrapper.valid()).isFalse(); 48 | wrapper.writeTo(out); 49 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1); 50 | 51 | verify(innerFilter, times(1)).expectedInsertions(); 52 | verify(innerFilter, times(1)).fpp(); 53 | verify(innerFilter, times(1)).mightContain(testingValue); 54 | verify(innerFilter, times(1)).set(testingValue); 55 | verify(innerFilter, times(1)).valid(); 56 | verify(innerFilter, times(1)).writeTo(out); 57 | } 58 | 59 | @Test 60 | public void testUpdateCounter() { 61 | final String testingValue = "testingValue"; 62 | when(innerFilter.set(testingValue)).thenReturn(false); 63 | assertThat(filterUpdateTimesCounter.sum()).isZero(); 64 | assertThat(wrapper.set(testingValue)).isFalse(); 65 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1); 66 | } 67 | 68 | @Test 69 | public void testWriteToThrowException() throws IOException { 70 | final OutputStream out = new ByteArrayOutputStream(); 71 | final IOException expectedException = new IOException("expected IO exception"); 72 | doThrow(expectedException).when(innerFilter).writeTo(out); 73 | 74 | assertThatThrownBy(() -> wrapper.writeTo(out)).isSameAs(expectedException); 75 | verify(innerFilter, times(1)).writeTo(out); 76 | } 77 | 78 | @Test 79 | public void testToJson() { 80 | final Duration validPeriodAfterAccess = Duration.ofSeconds(10); 81 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 82 | config.setValidPeriodAfterAccess(validPeriodAfterAccess); 83 | final GuavaBloomFilter innerFilter = new GuavaBloomFilterFactory().createFilter(config); 84 | final ObjectMapper mapper = new ObjectMapper(); 85 | final String expectedJson = mapper.valueToTree(innerFilter).toString(); 86 | 87 | final CountUpdateBloomFilterWrapper filter = new CountUpdateBloomFilterWrapper(innerFilter, filterUpdateTimesCounter); 88 | assertThat(mapper.valueToTree(filter).toString()).isEqualTo(expectedJson); 89 | } 90 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/ErrorsTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class ErrorsTest { 8 | @Test 9 | public void testUnknownError() { 10 | assertThat(Errors.UNKNOWN_ERROR.buildErrorInfoInJson().toString()) 11 | .isEqualTo("{\"error\":\"unknown error\",\"code\":-1}"); 12 | 13 | assertThat(Errors.UNKNOWN_ERROR.buildErrorInfoInJson("error msg").toString()) 14 | .isEqualTo("{\"error\":\"error msg\",\"code\":-1}"); 15 | } 16 | 17 | @Test 18 | public void testBadParameter() { 19 | assertThat(Errors.BAD_PARAMETER.buildErrorInfoInJson().toString()) 20 | .isEqualTo("{\"error\":\"invalid parameter\",\"code\":1}"); 21 | 22 | assertThat(Errors.BAD_PARAMETER.buildErrorInfoInJson("error msg").toString()) 23 | .isEqualTo("{\"error\":\"error msg\",\"code\":1}"); 24 | } 25 | 26 | @Test 27 | public void testFilterNotFound() { 28 | assertThat(Errors.FILTER_NOT_FOUND.buildErrorInfoInJson().toString()) 29 | .isEqualTo("{\"error\":\"filter not found\",\"code\":2}"); 30 | 31 | assertThat(Errors.FILTER_NOT_FOUND.buildErrorInfoInJson("error msg").toString()) 32 | .isEqualTo("{\"error\":\"error msg\",\"code\":2}"); 33 | } 34 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/ExpirableBloomFilterConfigTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Test; 4 | 5 | import java.time.Duration; 6 | import java.util.concurrent.ThreadLocalRandom; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | 12 | public class ExpirableBloomFilterConfigTest { 13 | @Test 14 | public void testGetAndSetValidPeriodAfterCreate() { 15 | final int expectedValidPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 16 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 17 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Configuration.defaultValidPeriodAfterCreate()); 18 | assertThat(config.setValidPeriodAfterCreate(Duration.ofSeconds(expectedValidPeriod))).isSameAs(config); 19 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Duration.ofSeconds(expectedValidPeriod)); 20 | } 21 | 22 | @Test 23 | public void testSetNegativeValidPeriodAfterCreate() { 24 | final int invalidValidPeriod = -1 * Math.abs(ThreadLocalRandom.current().nextInt()); 25 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 26 | final Duration old = config.validPeriodAfterCreate(); 27 | assertThatThrownBy(() -> config.setValidPeriodAfterCreate(Duration.ofSeconds(invalidValidPeriod))) 28 | .isInstanceOf(BadParameterException.class) 29 | .hasMessageContaining("invalid parameter"); 30 | assertThat(config.validPeriodAfterCreate()).isSameAs(old); 31 | } 32 | 33 | @Test 34 | public void testSetZeroValidPeriodAfterCreate() { 35 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 36 | final Duration old = config.validPeriodAfterCreate(); 37 | assertThatThrownBy(() -> config.setValidPeriodAfterCreate(Duration.ofSeconds(0))) 38 | .isInstanceOf(BadParameterException.class) 39 | .hasMessageContaining("invalid parameter"); 40 | 41 | assertThat(config.validPeriodAfterCreate()).isEqualTo(old); 42 | } 43 | 44 | @Test 45 | public void testDefaultValidPeriodAfterAccess() { 46 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 47 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Configuration.defaultValidPeriodAfterCreate()); 48 | assertThat(config.validPeriodAfterAccess()).isNull(); 49 | } 50 | 51 | @Test 52 | public void testGetAndSetValidPeriodAfterAccess() { 53 | final int expectedValidPeriodAfterAccess = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 54 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 55 | 56 | assertThat(config.setValidPeriodAfterAccess(Duration.ofSeconds(expectedValidPeriodAfterAccess))).isSameAs(config); 57 | assertThat(config.validPeriodAfterAccess()).isEqualTo(Duration.ofSeconds(expectedValidPeriodAfterAccess)); 58 | } 59 | 60 | @Test 61 | public void testSetNegativeValidPeriodAfterAccess() { 62 | final int invalidValidPeriod = -1 * Math.abs(ThreadLocalRandom.current().nextInt()); 63 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 64 | assertThatThrownBy(() -> config.setValidPeriodAfterAccess(Duration.ofSeconds(invalidValidPeriod))) 65 | .isInstanceOf(BadParameterException.class) 66 | .hasMessageContaining("invalid parameter"); 67 | assertThat(config.validPeriodAfterAccess()).isNull(); 68 | } 69 | 70 | @Test 71 | public void testSetZeroValidPeriodAfterAccess() { 72 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 73 | assertThatThrownBy(() -> config.setValidPeriodAfterAccess(Duration.ofSeconds(0))) 74 | .isInstanceOf(BadParameterException.class) 75 | .hasMessageContaining("invalid parameter"); 76 | 77 | assertThat(config.validPeriodAfterAccess()).isNull(); 78 | } 79 | 80 | @Test 81 | public void testEquals() { 82 | final int validPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 83 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1); 84 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 85 | final ExpirableBloomFilterConfig filterA = new ExpirableBloomFilterConfig() 86 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod)) 87 | .setFpp(fpp) 88 | .setExpectedInsertions(expectedInsertions); 89 | 90 | final ExpirableBloomFilterConfig filterB = new ExpirableBloomFilterConfig() 91 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod)) 92 | .setFpp(fpp) 93 | .setExpectedInsertions(expectedInsertions); 94 | 95 | assertThat(filterA).isEqualTo(filterB); 96 | } 97 | 98 | @Test 99 | public void testHashCode() { 100 | final int validPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 101 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1); 102 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 103 | final ExpirableBloomFilterConfig filterA = new ExpirableBloomFilterConfig() 104 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod)) 105 | .setFpp(fpp) 106 | .setExpectedInsertions(expectedInsertions); 107 | 108 | final ExpirableBloomFilterConfig filterB = new ExpirableBloomFilterConfig() 109 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod)) 110 | .setFpp(fpp) 111 | .setExpectedInsertions(expectedInsertions); 112 | 113 | assertThat(filterA.hashCode()).isEqualTo(filterB.hashCode()); 114 | } 115 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/FilterRecordInputStreamTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.io.EOFException; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.ByteBuffer; 12 | import java.nio.channels.FileChannel; 13 | import java.nio.channels.GatheringByteChannel; 14 | import java.nio.file.StandardOpenOption; 15 | import java.util.List; 16 | 17 | import static cn.leancloud.filter.service.FilterRecord.*; 18 | import static cn.leancloud.filter.service.TestingUtils.generateFilterRecords; 19 | import static cn.leancloud.filter.service.TestingUtils.generateSingleFilterRecord; 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 22 | 23 | public class FilterRecordInputStreamTest { 24 | private final GuavaBloomFilterFactory factory = new GuavaBloomFilterFactory(); 25 | 26 | private FileChannel writeRecordChannel; 27 | private File tempFile; 28 | private String tempDir; 29 | 30 | @Before 31 | public void setUp() throws Exception { 32 | tempDir = System.getProperty("java.io.tmpdir", "/tmp") + 33 | File.separator + "filter_service_" + System.nanoTime(); 34 | FileUtils.forceMkdir(new File(tempDir)); 35 | tempFile = new File(tempDir + File.separator + "filter_record_test"); 36 | writeRecordChannel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ); 37 | } 38 | 39 | @After 40 | public void tearDown() throws Exception { 41 | writeRecordChannel.close(); 42 | FileUtils.forceDelete(new File(tempDir)); 43 | } 44 | 45 | @Test 46 | public void testReadWriteMultiFilterRecord() throws Exception { 47 | final List> records = generateFilterRecords(100); 48 | for (FilterRecord record : records) { 49 | record.writeFullyTo(writeRecordChannel); 50 | } 51 | 52 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 53 | for (FilterRecord expectRecord : records) { 54 | assertThat(stream.nextFilterRecord()).isEqualTo(expectRecord); 55 | } 56 | 57 | assertThat(stream.nextFilterRecord()).isNull(); 58 | } 59 | } 60 | 61 | @Test 62 | public void testShortReadFilterHeader() throws Exception { 63 | final FilterRecord record = generateSingleFilterRecord(); 64 | record.writeFullyTo(writeRecordChannel); 65 | writeRecordChannel.truncate(HEADER_OVERHEAD - 1); 66 | 67 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 68 | assertThatThrownBy(stream::nextFilterRecord).hasMessage(UnfinishedFilterException.shortReadFilterHeader(tempFile.toString(), 1).getMessage()); 69 | } 70 | } 71 | 72 | @Test 73 | public void testShortReadFilterBody() throws Exception { 74 | final FilterRecord record = generateSingleFilterRecord(); 75 | record.writeFullyTo(writeRecordChannel); 76 | writeRecordChannel.truncate(writeRecordChannel.size() - 1); 77 | 78 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 79 | assertThatThrownBy(stream::nextFilterRecord).hasMessage(UnfinishedFilterException.shortReadFilterBody(tempFile.toString(), 1).getMessage()); 80 | } 81 | } 82 | 83 | @Test 84 | public void testBadMagic() throws Exception { 85 | final FilterRecord record = generateSingleFilterRecord(); 86 | record.writeFullyTo(writeRecordChannel); 87 | 88 | overwriteFirstFilterRecordMagic((byte) 101); 89 | 90 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 91 | assertThatThrownBy(stream::nextFilterRecord) 92 | .isInstanceOf(InvalidFilterException.class) 93 | .hasMessageContaining("read unknown Magic: 101 from position"); 94 | } 95 | } 96 | 97 | @Test 98 | public void testBadCrc() throws Exception { 99 | final FilterRecord record = generateSingleFilterRecord(); 100 | record.writeFullyTo(writeRecordChannel); 101 | 102 | overwriteFirstFilterRecordCrc(101); 103 | 104 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 105 | assertThatThrownBy(stream::nextFilterRecord) 106 | .isInstanceOf(InvalidFilterException.class) 107 | .hasMessageContaining("got unmatched crc when read filter from position"); 108 | } 109 | } 110 | 111 | @Test 112 | public void testEOF() throws Exception { 113 | final FilterRecord record = generateSingleFilterRecord(); 114 | record.writeFullyTo(writeRecordChannel); 115 | 116 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) { 117 | writeRecordChannel.truncate(writeRecordChannel.size() - 1); 118 | assertThatThrownBy(stream::nextFilterRecord) 119 | .isInstanceOf(EOFException.class) 120 | .hasMessageContaining("Failed to read `FilterRecord` from file channel"); 121 | } 122 | } 123 | 124 | private void overwriteFirstFilterRecordMagic(byte magic) throws IOException { 125 | ByteBuffer buffer = readFirstHeader(); 126 | buffer.flip(); 127 | buffer.put(MAGIC_OFFSET, magic); 128 | writeRecordChannel.position(0); 129 | writeFullyTo(writeRecordChannel, buffer); 130 | } 131 | 132 | private void overwriteFirstFilterRecordCrc(int crc) throws IOException { 133 | ByteBuffer buffer = readFirstHeader(); 134 | buffer.flip(); 135 | buffer.putInt(CRC_OFFSET, crc); 136 | writeRecordChannel.position(0); 137 | writeFullyTo(writeRecordChannel, buffer); 138 | } 139 | 140 | private int writeFullyTo(GatheringByteChannel channel, ByteBuffer buffer) throws IOException { 141 | buffer.mark(); 142 | int written = 0; 143 | while (written < buffer.limit()) { 144 | written = channel.write(buffer); 145 | } 146 | buffer.reset(); 147 | return written; 148 | } 149 | 150 | private ByteBuffer readFirstHeader() throws IOException { 151 | ByteBuffer buffer = ByteBuffer.allocate(HEADER_OVERHEAD); 152 | FilterRecordInputStream.readFullyOrFail(writeRecordChannel, buffer, 0); 153 | return buffer; 154 | } 155 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/FilterRecordTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.nio.channels.FileChannel; 10 | import java.nio.file.StandardOpenOption; 11 | import java.time.Duration; 12 | import java.time.ZoneOffset; 13 | import java.time.ZonedDateTime; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | public class FilterRecordTest { 18 | private static final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 19 | private static final int expectedInsertions = 1000000; 20 | private static final double fpp = 0.0001; 21 | private static final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 22 | private static final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 23 | private static final String testingFilterName = "testing_filter"; 24 | 25 | private File tempFile; 26 | private String tempDir; 27 | 28 | @Before 29 | public void setUp() throws Exception { 30 | tempDir = System.getProperty("java.io.tmpdir", "/tmp") + 31 | File.separator + "filter_service_" + System.nanoTime(); 32 | FileUtils.forceMkdir(new File(tempDir)); 33 | tempFile = new File(tempDir + File.separator + "filter_record_test"); 34 | } 35 | 36 | @After 37 | public void tearDown() throws Exception { 38 | FileUtils.forceDelete(new File(tempDir)); 39 | } 40 | 41 | @Test 42 | public void testReadWriteFilterRecord() throws Exception { 43 | final GuavaBloomFilter filter = new GuavaBloomFilter( 44 | expectedInsertions, 45 | fpp, 46 | creation, 47 | expiration, 48 | validPeriodAfterAccess); 49 | final FilterRecord record = new FilterRecord<>(testingFilterName, filter); 50 | final FileChannel channel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ); 51 | 52 | record.writeFullyTo(channel); 53 | 54 | final FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), new GuavaBloomFilterFactory()); 55 | assertThat(stream.nextFilterRecord()).isEqualTo(new FilterRecord<>(testingFilterName, filter)); 56 | } 57 | 58 | @Test 59 | public void testHashAndEquals() { 60 | final GuavaBloomFilter filter = new GuavaBloomFilter( 61 | expectedInsertions, 62 | fpp, 63 | creation, 64 | expiration, 65 | validPeriodAfterAccess); 66 | final FilterRecord record = new FilterRecord<>(testingFilterName, filter); 67 | final FilterRecord record2 = new FilterRecord<>(testingFilterName, filter); 68 | assertThat(record.hashCode()).isEqualTo(record2.hashCode()); 69 | assertThat(record.equals(record2)).isTrue(); 70 | } 71 | 72 | @Test 73 | public void testHashAndEquals2() { 74 | final GuavaBloomFilter filter = new GuavaBloomFilter( 75 | expectedInsertions, 76 | fpp, 77 | creation, 78 | expiration, 79 | validPeriodAfterAccess); 80 | final FilterRecord record = new FilterRecord<>("record1", filter); 81 | final FilterRecord record2 = new FilterRecord<>("record2", filter); 82 | assertThat(record.hashCode()).isNotEqualTo(record2.hashCode()); 83 | assertThat(record.equals(record2)).isFalse(); 84 | } 85 | 86 | @Test 87 | public void testHashAndEquals3() { 88 | final FilterRecord record = new FilterRecord<>(testingFilterName, 89 | new GuavaBloomFilter( 90 | expectedInsertions, 91 | 0.01, 92 | creation, 93 | expiration, 94 | validPeriodAfterAccess)); 95 | final FilterRecord record2 = new FilterRecord<>(testingFilterName, 96 | new GuavaBloomFilter( 97 | expectedInsertions, 98 | 0.001, 99 | creation, 100 | expiration, 101 | validPeriodAfterAccess)); 102 | assertThat(record.hashCode()).isNotEqualTo(record2.hashCode()); 103 | assertThat(record.equals(record2)).isFalse(); 104 | } 105 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/FilterServiceFileUtilsTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | 4 | import org.apache.commons.io.FileUtils; 5 | import org.junit.Test; 6 | 7 | import java.io.File; 8 | import java.nio.file.NoSuchFileException; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | 12 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 13 | 14 | public class FilterServiceFileUtilsTest { 15 | 16 | @Test 17 | public void atomicMoveWithFallbackFailed() throws Exception { 18 | final String tempDir = System.getProperty("java.io.tmpdir", "/tmp") + 19 | File.separator + "filter_service_" + System.nanoTime(); 20 | FileUtils.forceMkdir(new File(tempDir)); 21 | Path a = Paths.get(tempDir).resolve("path_a"); 22 | Path b = Paths.get(tempDir).resolve("path_b"); 23 | assertThatThrownBy(() -> FilterServiceFileUtils.atomicMoveWithFallback(a, b)) 24 | .isInstanceOf(NoSuchFileException.class); 25 | FileUtils.forceDelete(new File(tempDir)); 26 | } 27 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/GuavaBloomFilterTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.ByteArrayOutputStream; 8 | import java.time.Duration; 9 | import java.time.ZoneOffset; 10 | import java.time.ZonedDateTime; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class GuavaBloomFilterTest { 15 | private static final GuavaBloomFilterFactory testingFactory = new GuavaBloomFilterFactory(); 16 | private static final ExpirableBloomFilterConfig defaultTestingConfig = new ExpirableBloomFilterConfig(); 17 | 18 | @Test 19 | public void testGetters() { 20 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 21 | final int expectedInsertions = 1000000; 22 | final double fpp = 0.0001; 23 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 24 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 25 | final GuavaBloomFilter filter = new GuavaBloomFilter( 26 | expectedInsertions, 27 | fpp, 28 | creation, 29 | expiration, 30 | validPeriodAfterAccess); 31 | 32 | assertThat(filter.fpp()).isEqualTo(fpp); 33 | assertThat(filter.expectedInsertions()).isEqualTo(expectedInsertions); 34 | assertThat(filter.expiration()).isEqualTo(expiration); 35 | assertThat(filter.validPeriodAfterAccess()).isEqualTo(validPeriodAfterAccess); 36 | assertThat(filter.created()).isEqualTo(creation); 37 | assertThat(filter.expired()).isFalse(); 38 | } 39 | 40 | @Test 41 | public void testMightContain() { 42 | final String testingValue = "SomeValue"; 43 | final GuavaBloomFilter filter = testingFactory.createFilter(defaultTestingConfig); 44 | assertThat(filter.mightContain(testingValue)).isFalse(); 45 | assertThat(filter.set(testingValue)).isTrue(); 46 | assertThat(filter.mightContain(testingValue)).isTrue(); 47 | } 48 | 49 | @Test 50 | public void testExpiredFilter() { 51 | final AdjustableTimer timer = new AdjustableTimer(); 52 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 53 | final ZonedDateTime expiration = creation.plusSeconds(10); 54 | final GuavaBloomFilter filter = new GuavaBloomFilter( 55 | 1000, 56 | 0.001, 57 | creation, 58 | expiration, 59 | null, 60 | timer); 61 | timer.setNow(expiration); 62 | assertThat(filter.expired()).isFalse(); 63 | timer.setNow(expiration.plusSeconds(1)); 64 | assertThat(filter.expired()).isTrue(); 65 | } 66 | 67 | @Test 68 | public void testExtendExpirationOnSet() { 69 | final String testingValue = "SomeValue"; 70 | final AdjustableTimer timer = new AdjustableTimer(); 71 | final Duration validPeriodAfterAccess = Duration.ofSeconds(5); 72 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 73 | final ZonedDateTime expiration = creation.plus(validPeriodAfterAccess); 74 | final GuavaBloomFilter filter = new GuavaBloomFilter( 75 | 1000, 76 | 0.001, 77 | creation, 78 | expiration, 79 | validPeriodAfterAccess, 80 | timer); 81 | timer.setNow(expiration); 82 | assertThat(filter.expired()).isFalse(); 83 | filter.set(testingValue); 84 | timer.setNow(expiration.plus(Duration.ofSeconds(1))); 85 | assertThat(filter.expired()).isFalse(); 86 | } 87 | 88 | @Test 89 | public void testExtendExpirationOnMightContain() { 90 | final String testingValue = "SomeValue"; 91 | final AdjustableTimer timer = new AdjustableTimer(); 92 | final Duration validPeriodAfterAccess = Duration.ofSeconds(5); 93 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 94 | final ZonedDateTime expiration = creation.plus(validPeriodAfterAccess); 95 | final GuavaBloomFilter filter = new GuavaBloomFilter( 96 | 1000, 97 | 0.001, 98 | creation, 99 | expiration, 100 | validPeriodAfterAccess, 101 | timer); 102 | timer.setNow(expiration); 103 | assertThat(filter.expired()).isFalse(); 104 | filter.mightContain(testingValue); 105 | timer.setNow(expiration.plus(Duration.ofSeconds(1))); 106 | assertThat(filter.expired()).isFalse(); 107 | } 108 | 109 | @Test 110 | public void testToJson() throws Exception { 111 | final Duration validPeriodAfterAccess = Duration.ofSeconds(10); 112 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(defaultTestingConfig); 113 | config.setValidPeriodAfterAccess(validPeriodAfterAccess); 114 | 115 | final GuavaBloomFilter expectedFilter = testingFactory.createFilter(config); 116 | final ObjectMapper mapper = new ObjectMapper(); 117 | 118 | final String json = mapper.valueToTree(expectedFilter).toString(); 119 | final GuavaBloomFilter filter = new ObjectMapper().readerFor(GuavaBloomFilter.class).readValue(json); 120 | assertThat(filter).isEqualTo(expectedFilter); 121 | } 122 | 123 | @Test 124 | public void testSerializationWithoutValidPeriodAfterAccess() throws Exception { 125 | final int expectedInsertions = 1000000; 126 | final double fpp = 0.0001; 127 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 128 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 129 | final GuavaBloomFilter expect = new GuavaBloomFilter( 130 | expectedInsertions, 131 | fpp, 132 | creation, 133 | expiration, 134 | null); 135 | 136 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 137 | expect.writeTo(out); 138 | 139 | final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); 140 | final GuavaBloomFilter actualFilter = GuavaBloomFilter.readFrom(in); 141 | 142 | assertThat(actualFilter).isEqualTo(expect); 143 | assertThat(actualFilter.fpp()).isEqualTo(fpp); 144 | assertThat(actualFilter.expectedInsertions()).isEqualTo(expectedInsertions); 145 | assertThat(actualFilter.expiration().toEpochSecond()).isEqualTo(expiration.toEpochSecond()); 146 | assertThat(actualFilter.validPeriodAfterAccess()).isNull(); 147 | assertThat(actualFilter.created().toEpochSecond()).isEqualTo(creation.toEpochSecond()); 148 | assertThat(actualFilter.expired()).isFalse(); 149 | } 150 | 151 | @Test 152 | public void testSerializationWithValidPeriodAfterAccess() throws Exception { 153 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 154 | final int expectedInsertions = 1000000; 155 | final double fpp = 0.0001; 156 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 157 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 158 | final GuavaBloomFilter expect = new GuavaBloomFilter( 159 | expectedInsertions, 160 | fpp, 161 | creation, 162 | expiration, 163 | validPeriodAfterAccess); 164 | 165 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 166 | expect.writeTo(out); 167 | 168 | final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); 169 | final GuavaBloomFilter actualFilter = testingFactory.readFrom(in); 170 | 171 | assertThat(actualFilter).isEqualTo(expect); 172 | assertThat(actualFilter.fpp()).isEqualTo(fpp); 173 | assertThat(actualFilter.expectedInsertions()).isEqualTo(expectedInsertions); 174 | assertThat(actualFilter.expiration().toEpochSecond()).isEqualTo(expiration.toEpochSecond()); 175 | assertThat(actualFilter.validPeriodAfterAccess()).isEqualTo(validPeriodAfterAccess); 176 | assertThat(actualFilter.created().toEpochSecond()).isEqualTo(creation.toEpochSecond()); 177 | assertThat(actualFilter.expired()).isFalse(); 178 | } 179 | 180 | @Test 181 | public void testHashcodeAndEquals() { 182 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 183 | final int expectedInsertions = 1000000; 184 | final double fpp = 0.0001; 185 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 186 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 187 | final GuavaBloomFilter filter = new GuavaBloomFilter( 188 | expectedInsertions, 189 | fpp, 190 | creation, 191 | expiration, 192 | validPeriodAfterAccess); 193 | 194 | final GuavaBloomFilter filter2 = new GuavaBloomFilter( 195 | expectedInsertions, 196 | fpp, 197 | creation, 198 | expiration, 199 | validPeriodAfterAccess); 200 | 201 | assertThat(filter.hashCode()).isEqualTo(filter2.hashCode()); 202 | assertThat(filter.equals(filter2)).isTrue(); 203 | } 204 | 205 | @Test 206 | public void testHashcodeAndEquals2() { 207 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 208 | final double fpp = 0.0001; 209 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 210 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 211 | final GuavaBloomFilter filter = new GuavaBloomFilter( 212 | 1001, 213 | fpp, 214 | creation, 215 | expiration, 216 | validPeriodAfterAccess); 217 | 218 | final GuavaBloomFilter filter2 = new GuavaBloomFilter( 219 | 1002, 220 | fpp, 221 | creation, 222 | expiration, 223 | validPeriodAfterAccess); 224 | 225 | assertThat(filter.hashCode()).isNotEqualTo(filter2.hashCode()); 226 | assertThat(filter.equals(filter2)).isFalse(); 227 | } 228 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/InvalidBloomFilterPurgatoryTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Test; 4 | import org.mockito.Mockito; 5 | 6 | import java.time.Duration; 7 | import java.time.ZoneOffset; 8 | import java.time.ZonedDateTime; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class InvalidBloomFilterPurgatoryTest { 13 | private static final String testingFilterName = "TestingFilterName"; 14 | @Test 15 | public void testPurge() { 16 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(); 17 | final ZonedDateTime creationTime = ZonedDateTime.now(ZoneOffset.UTC).minus(Duration.ofSeconds(10)); 18 | final ZonedDateTime expirationTime = creationTime.plusSeconds(5); 19 | final GuavaBloomFilterFactory mockedFactory = Mockito.mock(GuavaBloomFilterFactory.class); 20 | 21 | Mockito.when(mockedFactory.createFilter(config)) 22 | .thenReturn(new GuavaBloomFilter( 23 | Configuration.defaultExpectedInsertions(), 24 | Configuration.defaultFalsePositiveProbability(), 25 | creationTime, 26 | expirationTime, 27 | null)); 28 | 29 | final BloomFilterManagerImpl manager = new BloomFilterManagerImpl<>(mockedFactory); 30 | final GuavaBloomFilter filter = manager.createFilter(testingFilterName, config).getFilter(); 31 | final InvalidBloomFilterPurgatory purgatory = 32 | new InvalidBloomFilterPurgatory<>(manager); 33 | 34 | assertThat(filter.expired()).isTrue(); 35 | assertThat(manager.getFilter(testingFilterName)).isSameAs(filter); 36 | 37 | purgatory.purge(); 38 | 39 | assertThat(manager.getFilter(testingFilterName)).isNull(); 40 | } 41 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentFilterByOutputStreamBenchmark.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.utils.ChecksumedBufferedOutputStream; 4 | import org.openjdk.jmh.annotations.*; 5 | import org.openjdk.jmh.runner.Runner; 6 | import org.openjdk.jmh.runner.options.Options; 7 | import org.openjdk.jmh.runner.options.OptionsBuilder; 8 | import org.openjdk.jmh.runner.options.TimeValue; 9 | 10 | import java.io.BufferedOutputStream; 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.nio.ByteBuffer; 14 | import java.nio.channels.Channels; 15 | import java.nio.channels.FileChannel; 16 | import java.nio.channels.GatheringByteChannel; 17 | import java.nio.file.Paths; 18 | import java.nio.file.StandardOpenOption; 19 | import java.time.Duration; 20 | import java.time.ZoneOffset; 21 | import java.time.ZonedDateTime; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | @State(Scope.Thread) 25 | @BenchmarkMode({Mode.Throughput}) 26 | @OutputTimeUnit(TimeUnit.SECONDS) 27 | @Threads(value = 1) 28 | public class PersistentFilterByOutputStreamBenchmark { 29 | GuavaBloomFilter filter; 30 | FileChannel fileChannel; 31 | 32 | @Setup 33 | public void setup() throws Exception { 34 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 35 | final int expectedInsertions = 10000000; 36 | final double fpp = 0.001; 37 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 38 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 39 | filter = new GuavaBloomFilter( 40 | expectedInsertions, 41 | fpp, 42 | creation, 43 | expiration, 44 | validPeriodAfterAccess); 45 | fileChannel = FileChannel.open(Paths.get("/dev/null"), StandardOpenOption.CREATE, 46 | StandardOpenOption.WRITE, StandardOpenOption.READ); 47 | } 48 | 49 | @TearDown 50 | public void teardown() throws Exception { 51 | fileChannel.close(); 52 | } 53 | 54 | @Benchmark 55 | public int testByteArrayOutputStream() throws Exception { 56 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 57 | filter.writeTo(out); 58 | ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray()); 59 | return writeFullyTo(fileChannel, buffer); 60 | } 61 | 62 | private int writeFullyTo(GatheringByteChannel channel, ByteBuffer buffer) throws IOException { 63 | buffer.mark(); 64 | int written = 0; 65 | while (written < buffer.limit()) { 66 | written = channel.write(buffer); 67 | } 68 | buffer.reset(); 69 | return written; 70 | } 71 | 72 | @Benchmark 73 | public int testBufferedOutputStream() throws Exception { 74 | final long pos = fileChannel.position(); 75 | final BufferedOutputStream bout = new BufferedOutputStream(Channels.newOutputStream(fileChannel), 102400); 76 | filter.writeTo(bout); 77 | bout.flush(); 78 | return (int) (fileChannel.position() - pos); 79 | } 80 | 81 | @Benchmark 82 | public int testChecksumedBufferedOutputStream() throws Exception { 83 | final long pos = fileChannel.position(); 84 | final ChecksumedBufferedOutputStream bout = new ChecksumedBufferedOutputStream(Channels.newOutputStream(fileChannel), 102400); 85 | filter.writeTo(bout); 86 | bout.flush(); 87 | return (int) (fileChannel.position() - pos); 88 | } 89 | 90 | public static void main(String[] args) throws Exception { 91 | Options opt = new OptionsBuilder() 92 | .include(PersistentFilterByOutputStreamBenchmark.class.getSimpleName()) 93 | .warmupIterations(3) 94 | .warmupTime(TimeValue.seconds(10)) 95 | .measurementIterations(3) 96 | .measurementTime(TimeValue.seconds(10)) 97 | .forks(1) 98 | .build(); 99 | 100 | new Runner(opt).run(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentFiltersJobTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import cn.leancloud.filter.service.Configuration.TriggerPersistenceCriteria; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import io.micrometer.core.instrument.Timer; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import java.io.IOException; 10 | import java.time.Duration; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.ScheduledFuture; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.LongAdder; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | import static org.mockito.ArgumentMatchers.*; 22 | import static org.mockito.Mockito.*; 23 | import static org.mockito.Mockito.times; 24 | 25 | @SuppressWarnings("unchecked") 26 | public class PersistentFiltersJobTest { 27 | private PersistentFiltersJob job; 28 | private BloomFilterManager bloomFilterManager; 29 | private PersistentManager persistentManager; 30 | private LongAdder filterUpdateTimesCounter; 31 | private TriggerPersistenceCriteria criteria = new TriggerPersistenceCriteria(Duration.ofSeconds(1), 10); 32 | 33 | @Before 34 | public void setUp() { 35 | bloomFilterManager = mock(BloomFilterManager.class); 36 | persistentManager = mock(PersistentManager.class); 37 | filterUpdateTimesCounter = new LongAdder(); 38 | job = new PersistentFiltersJob(bloomFilterManager, persistentManager, filterUpdateTimesCounter, criteria); 39 | } 40 | 41 | @Test 42 | public void testCriteriaNotMeet() throws IOException { 43 | filterUpdateTimesCounter.reset(); 44 | filterUpdateTimesCounter.add(5); 45 | job.run(); 46 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(5); 47 | verify(persistentManager, never()).freezeAllFilters(bloomFilterManager); 48 | } 49 | 50 | @Test 51 | public void testPersistentWhenCriteriaMeet() throws IOException { 52 | filterUpdateTimesCounter.add(100); 53 | job.run(); 54 | assertThat(filterUpdateTimesCounter.sum()).isZero(); 55 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager); 56 | } 57 | 58 | @Test 59 | public void testPersistentThrowsException() throws IOException { 60 | final Exception exception = new IOException("expected exception"); 61 | doThrow(exception).when(persistentManager).freezeAllFilters(bloomFilterManager); 62 | filterUpdateTimesCounter.add(100); 63 | // eat exception 64 | job.run(); 65 | assertThat(filterUpdateTimesCounter.sum()).isZero(); 66 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager); 67 | } 68 | 69 | @Test 70 | public void testPersistentThrowsError() throws IOException { 71 | final Error error = new Error("expected error"); 72 | doThrow(error).when(persistentManager).freezeAllFilters(bloomFilterManager); 73 | filterUpdateTimesCounter.add(100); 74 | 75 | assertThatThrownBy(() -> job.run()).isSameAs(error); 76 | assertThat(filterUpdateTimesCounter.sum()).isZero(); 77 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager); 78 | } 79 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentManagerTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.channels.FileChannel; 11 | import java.nio.channels.OverlappingFileLockException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.nio.file.StandardOpenOption; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static cn.leancloud.filter.service.TestingUtils.generateFilterRecords; 19 | import static cn.leancloud.filter.service.TestingUtils.generateInvalidFilter; 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 22 | import static org.mockito.ArgumentMatchers.anyCollection; 23 | import static org.mockito.Mockito.*; 24 | 25 | @SuppressWarnings("unchecked") 26 | public class PersistentManagerTest { 27 | private BloomFilterFactory factory; 28 | private Path tempDirPath; 29 | private BloomFilterManager filterManager; 30 | private PersistentManager manager; 31 | 32 | @Before 33 | public void setUp() throws Exception { 34 | final String tempDir = System.getProperty("java.io.tmpdir", "/tmp") + 35 | File.separator + "filter_service_" + System.nanoTime(); 36 | tempDirPath = Paths.get(tempDir); 37 | FileUtils.forceMkdir(tempDirPath.toFile()); 38 | filterManager = mock(BloomFilterManager.class); 39 | factory = mock(GuavaBloomFilterFactory.class); 40 | when(factory.readFrom(any())).thenCallRealMethod(); 41 | manager = new PersistentManager<>(tempDirPath); 42 | } 43 | 44 | @After 45 | public void tearDown() throws Exception { 46 | manager.close(); 47 | FileUtils.forceDelete(tempDirPath.toFile()); 48 | } 49 | 50 | @Test 51 | public void testPersistentDirIsFile() throws Exception { 52 | FileChannel.open(tempDirPath.resolve("plain_file"), StandardOpenOption.CREATE, StandardOpenOption.WRITE); 53 | assertThatThrownBy(() -> new PersistentManager<>(tempDirPath.resolve("plain_file"))) 54 | .isInstanceOf(IllegalStateException.class) 55 | .hasMessageContaining("invalid persistent directory path, it's a regular file"); 56 | } 57 | 58 | @Test 59 | public void testLockAndReleaseLock() throws Exception { 60 | final Path lockPath = tempDirPath.resolve("lock_path"); 61 | final PersistentManager manager = new PersistentManager<>(lockPath); 62 | 63 | assertThatThrownBy(() -> new PersistentManager<>(lockPath)) 64 | .isInstanceOf(OverlappingFileLockException.class); 65 | 66 | manager.close(); 67 | 68 | new PersistentManager<>(lockPath); 69 | } 70 | 71 | @Test 72 | public void testMakeBaseDir() throws Exception { 73 | final Path newPath = tempDirPath.resolve("base_dir"); 74 | assertThat(newPath.toFile().exists()).isFalse(); 75 | new PersistentManager<>(newPath); 76 | assertThat(newPath.toFile().exists()).isTrue(); 77 | } 78 | 79 | @Test 80 | public void freezeAllFilters() throws IOException { 81 | final List> records = generateFilterRecords(10); 82 | when(filterManager.iterator()).thenReturn(records.iterator()); 83 | 84 | manager.freezeAllFilters(filterManager); 85 | 86 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>( 87 | manager.persistentFilePath(), 88 | new GuavaBloomFilterFactory())) { 89 | for (FilterRecord expectRecord : records) { 90 | assertThat(stream.nextFilterRecord()).isEqualTo(expectRecord); 91 | } 92 | 93 | assertThat(stream.nextFilterRecord()).isNull(); 94 | } 95 | } 96 | 97 | @Test 98 | public void testNoFilesToRecover() throws IOException { 99 | manager.recoverFilters(factory, true); 100 | verify(filterManager, never()).addFilters(anyCollection()); 101 | } 102 | 103 | @Test 104 | public void testRecoverFiltersFromFileNormalCase() throws IOException { 105 | final List> records = generateFilterRecords(10); 106 | when(filterManager.iterator()).thenReturn(records.iterator()); 107 | 108 | manager.freezeAllFilters(filterManager); 109 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records); 110 | } 111 | 112 | @Test 113 | public void testRecoverOnlyValidFiltersFromFile() throws IOException { 114 | final List> records = generateFilterRecords(10); 115 | final BloomFilter invalidFilter = generateInvalidFilter(); 116 | records.add(new FilterRecord("Invalid_Filter", invalidFilter)); 117 | when(filterManager.iterator()).thenReturn(records.iterator()); 118 | 119 | manager.freezeAllFilters(filterManager); 120 | assertThat(manager.recoverFilters(factory, false)) 121 | .isEqualTo(records.subList(0, records.size() - 1)); 122 | } 123 | 124 | @Test 125 | public void testDoNotAllowRecoverFromCorruptedFile() throws IOException { 126 | final List> records = generateFilterRecords(10); 127 | when(filterManager.iterator()).thenReturn(records.iterator()); 128 | manager.freezeAllFilters(filterManager); 129 | try (FileChannel channel = FileChannel.open(manager.persistentFilePath(), StandardOpenOption.WRITE)) { 130 | channel.truncate(channel.size() - 1); 131 | 132 | assertThatThrownBy(() -> manager.recoverFilters(factory, false)) 133 | .hasMessageContaining("failed to recover filters from:") 134 | .isInstanceOf(PersistentStorageException.class); 135 | } 136 | } 137 | 138 | @Test 139 | public void testAllowRecoverFromCorruptedFile() throws IOException { 140 | final List> records = generateFilterRecords(10); 141 | when(filterManager.iterator()).thenReturn(records.iterator()); 142 | manager.freezeAllFilters(filterManager); 143 | try (FileChannel channel = FileChannel.open(manager.persistentFilePath(), StandardOpenOption.WRITE)) { 144 | channel.truncate(channel.size() - 1); 145 | 146 | assertThat(manager.recoverFilters(factory, true)) 147 | .isEqualTo(records.subList(0, records.size() - 1)); 148 | } 149 | } 150 | 151 | @Test 152 | public void testRecoverOnlyFromTemporaryFile() throws IOException { 153 | final List> records = generateFilterRecords(10); 154 | when(filterManager.iterator()).thenReturn(records.iterator()); 155 | 156 | manager.freezeAllFilters(filterManager); 157 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), manager.temporaryPersistentFilePath()); 158 | 159 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records); 160 | } 161 | 162 | @Test 163 | public void testRecoverFromTemporaryFileAndNormalFile() throws IOException { 164 | final Path temporaryPath = manager.persistentFilePath().resolveSibling("tmp.bak"); 165 | final List> records = generateFilterRecords(20); 166 | final List> expected = new ArrayList<>(); 167 | expected.addAll(records); 168 | expected.addAll(records.subList(10, 20)); 169 | 170 | // prepare temporary file 171 | when(filterManager.iterator()).thenReturn(records.subList(10, 20).iterator()); 172 | manager.freezeAllFilters(filterManager); 173 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), temporaryPath); 174 | 175 | // prepare normal file 176 | when(filterManager.iterator()).thenReturn(records.iterator()); 177 | manager.freezeAllFilters(filterManager); 178 | 179 | FilterServiceFileUtils.atomicMoveWithFallback(temporaryPath, manager.temporaryPersistentFilePath()); 180 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(expected); 181 | } 182 | 183 | @Test 184 | public void testRecoverFromTemporaryFileFailed() throws IOException { 185 | final Path temporaryPath = manager.persistentFilePath().resolveSibling("tmp.bak"); 186 | final List> records = generateFilterRecords(20); 187 | 188 | // prepare temporary file 189 | when(filterManager.iterator()).thenReturn(generateFilterRecords(50, 20).iterator()); 190 | manager.freezeAllFilters(filterManager); 191 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), temporaryPath); 192 | 193 | // prepare normal file 194 | when(filterManager.iterator()).thenReturn(records.iterator()); 195 | manager.freezeAllFilters(filterManager); 196 | 197 | FilterServiceFileUtils.atomicMoveWithFallback(temporaryPath, manager.temporaryPersistentFilePath()); 198 | try (FileChannel channel = FileChannel.open(manager.temporaryPersistentFilePath(), StandardOpenOption.WRITE)) { 199 | channel.truncate(channel.size() - 1); 200 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records); 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/PurgeFiltersJobTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.mockito.Mockito.*; 7 | 8 | public class PurgeFiltersJobTest { 9 | private Purgatory purgatory; 10 | private PurgeFiltersJob job; 11 | 12 | @Before 13 | public void setUp() { 14 | purgatory = mock(Purgatory.class); 15 | job = new PurgeFiltersJob(purgatory); 16 | } 17 | 18 | @Test 19 | public void testPurge() { 20 | doNothing().when(purgatory).purge(); 21 | job.run(); 22 | verify(purgatory, times(1)).purge(); 23 | } 24 | 25 | @Test 26 | public void testPurgeThrowsException() { 27 | final RuntimeException ex = new RuntimeException("expected exception"); 28 | doThrow(ex).when(purgatory).purge(); 29 | job.run(); 30 | verify(purgatory, times(1)).purge(); 31 | } 32 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/ServerOptionsTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class ServerOptionsTest { 8 | @Test 9 | public void testServerOptions() { 10 | final String configFilePath = "/mnt/avos/logs"; 11 | final int port = 10101; 12 | final boolean docService = true; 13 | ServerOptions options = new ServerOptions(configFilePath, port, docService); 14 | assertThat(options.configFilePath()).isEqualTo(configFilePath); 15 | assertThat(options.port()).isEqualTo(port); 16 | assertThat(options.docServiceEnabled()).isTrue(); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/TestingUtils.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service; 2 | 3 | import java.time.Duration; 4 | import java.time.ZoneOffset; 5 | import java.time.ZonedDateTime; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public final class TestingUtils { 10 | private static final Duration validPeriodAfterAccess = Duration.ofSeconds(3); 11 | private static final int expectedInsertions = 1000000; 12 | private static final double fpp = 0.0001; 13 | private static final String testingFilterName = "testing_filter"; 14 | 15 | public static String numberString(Number num) { 16 | return "" + num; 17 | } 18 | 19 | public static FilterRecord generateSingleFilterRecord() { 20 | return generateFilterRecords(1).get(0); 21 | } 22 | 23 | public static List> generateFilterRecords(int size) { 24 | return generateFilterRecords(0, size); 25 | } 26 | 27 | public static List> generateFilterRecords(int startNumName, int size) { 28 | List> records = new ArrayList<>(size); 29 | for (int i = 0; i < size; ++i) { 30 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 31 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10)); 32 | final GuavaBloomFilter filter = new GuavaBloomFilter( 33 | expectedInsertions + i, 34 | fpp, 35 | creation, 36 | expiration, 37 | validPeriodAfterAccess); 38 | final FilterRecord record = new FilterRecord<>(testingFilterName + "_" + i, filter); 39 | records.add(record); 40 | } 41 | 42 | return records; 43 | } 44 | 45 | public static GuavaBloomFilter generateInvalidFilter() { 46 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC); 47 | final ZonedDateTime expiration = creation.minus(Duration.ofSeconds(10)); 48 | return new GuavaBloomFilter( 49 | expectedInsertions, 50 | fpp, 51 | creation, 52 | expiration, 53 | validPeriodAfterAccess); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/utils/AbstractIteratorTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.*; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | public class AbstractIteratorTest { 12 | static class ListIterator extends AbstractIterator { 13 | private List list; 14 | private int position = 0; 15 | 16 | public ListIterator(List l) { 17 | this.list = l; 18 | } 19 | 20 | public T makeNext() { 21 | if (position < list.size()) 22 | return list.get(position++); 23 | else 24 | return allDone(); 25 | } 26 | } 27 | 28 | @Test 29 | public void testIterator() { 30 | final int max = 10; 31 | final List l = new ArrayList(); 32 | for (int i = 0; i < max; i++) 33 | l.add(i); 34 | final ListIterator iter = new ListIterator(l); 35 | for (int i = 0; i < max; i++) { 36 | Integer value = i; 37 | assertThat(iter.peek()).isEqualTo(value); 38 | assertThat(iter.hasNext()).isTrue(); 39 | assertThat(iter.next()).isEqualTo(value); 40 | } 41 | assertThat(iter.hasNext()).isFalse(); 42 | } 43 | 44 | @Test 45 | public void testEmptyIterator() { 46 | final ListIterator iter = new ListIterator(Collections.emptyList()); 47 | assertThatThrownBy(iter::next).isInstanceOf(NoSuchElementException.class); 48 | assertThatThrownBy(iter::peek).isInstanceOf(NoSuchElementException.class); 49 | } 50 | 51 | @Test 52 | public void testMakeNextFailed() { 53 | final RuntimeException testingException = new RuntimeException("expected exception"); 54 | final Iterator iter = new AbstractIterator() { 55 | @Nullable 56 | @Override 57 | protected Object makeNext() { 58 | throw testingException; 59 | } 60 | }; 61 | 62 | assertThatThrownBy(iter::hasNext).isSameAs(testingException); 63 | assertThatThrownBy(iter::hasNext) 64 | .isInstanceOf(IllegalStateException.class) 65 | .hasMessage("iterator is in failed state"); 66 | } 67 | 68 | @Test 69 | public void testMakeNextReturnsNull() { 70 | final Iterator iter = new AbstractIterator() { 71 | @Nullable 72 | @Override 73 | protected Object makeNext() { 74 | return null; 75 | } 76 | }; 77 | 78 | assertThat(iter.hasNext()).isTrue(); 79 | assertThatThrownBy(iter::next) 80 | .isInstanceOf(IllegalStateException.class) 81 | .hasMessage("expected item but none found"); 82 | } 83 | 84 | @Test 85 | public void testRemove() { 86 | final ListIterator iter = new ListIterator(Collections.emptyList()); 87 | assertThatThrownBy(iter::remove) 88 | .isInstanceOf(UnsupportedOperationException.class) 89 | .hasMessage("removal not supported"); 90 | } 91 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/utils/Crc32CTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.zip.Checksum; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class Crc32CTest { 10 | @Test 11 | public void testUpdate() { 12 | final byte[] bytes = "Hello world".getBytes(); 13 | final int len = bytes.length; 14 | 15 | Checksum crc1 = Crc32C.create(); 16 | Checksum crc2 = Crc32C.create(); 17 | Checksum crc3 = Crc32C.create(); 18 | 19 | crc1.update(bytes, 0, len); 20 | for (int i = 0; i < len; i++) 21 | crc2.update(bytes[i]); 22 | crc3.update(bytes, 0, len / 2); 23 | crc3.update(bytes, len / 2, len - len / 2); 24 | 25 | assertThat(crc1.getValue()).isEqualTo(crc2.getValue()); 26 | assertThat(crc1.getValue()).isEqualTo(crc3.getValue()); 27 | } 28 | 29 | @Test 30 | public void testValue() { 31 | final byte[] bytes = "Some String".getBytes(); 32 | assertThat(Crc32C.compute(bytes, 0, bytes.length)).isEqualTo(608512271); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/java/cn/leancloud/filter/service/utils/JavaTest.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class JavaTest { 8 | @Test 9 | public void testJavaVersion() { 10 | Java.Version v = Java.parseVersion("9"); 11 | assertThat(v.majorVersion).isEqualTo(9); 12 | assertThat(v.minorVersion).isEqualTo(0); 13 | assertThat(v.isJava9Compatible()).isTrue(); 14 | 15 | v = Java.parseVersion("9.0.1"); 16 | assertThat(v.majorVersion).isEqualTo(9); 17 | assertThat(v.minorVersion).isEqualTo(0); 18 | assertThat(v.isJava9Compatible()).isTrue(); 19 | 20 | v = Java.parseVersion("9.0.0.15"); // Azul Zulu 21 | assertThat(v.majorVersion).isEqualTo(9); 22 | assertThat(v.minorVersion).isEqualTo(0); 23 | assertThat(v.isJava9Compatible()).isTrue(); 24 | 25 | v = Java.parseVersion("9.1"); 26 | assertThat(v.majorVersion).isEqualTo(9); 27 | assertThat(v.minorVersion).isEqualTo(1); 28 | assertThat(v.isJava9Compatible()).isTrue(); 29 | 30 | v = Java.parseVersion("1.8.0_152"); 31 | assertThat(v.majorVersion).isEqualTo(1); 32 | assertThat(v.minorVersion).isEqualTo(8); 33 | assertThat(v.isJava9Compatible()).isFalse(); 34 | 35 | 36 | v = Java.parseVersion("1.7.0_80"); 37 | assertThat(v.majorVersion).isEqualTo(1); 38 | assertThat(v.minorVersion).isEqualTo(7); 39 | assertThat(v.isJava9Compatible()).isFalse(); 40 | } 41 | } -------------------------------------------------------------------------------- /filter-service-core/src/test/resources/empty-configuration.yaml: -------------------------------------------------------------------------------- 1 | --- -------------------------------------------------------------------------------- /filter-service-core/src/test/resources/illegal-configuration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | Hi, I'm a illegal ymal file. -------------------------------------------------------------------------------- /filter-service-core/src/test/resources/testing-configuration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # the prefix for all the metrics generated from micrometer 3 | metricsPrefix: "filterServiceTest" 4 | 5 | # the default expected insertions to filter. If no value provided on creating filter, this value will be used 6 | defaultExpectedInsertions: 2000000 7 | 8 | # the default desired false positive probability for a filter. If no value provided on creating filter, this value will be used 9 | defaultFalsePositiveProbability: 0.0002 10 | 11 | # the default valid seconds after create for a filter. If no value provided on creating filter, this value will be used 12 | defaultValidSecondsAfterCreate: 172800 13 | 14 | # maximum allowed http/https concurrent connections 15 | maxHttpConnections: 2000 16 | 17 | # maximum allowed length of the content decoded at the session layer. the default value is 10 MB 18 | maxHttpRequestLength: 5242880 19 | 20 | # the default timeout of a request 21 | requestTimeoutSeconds: 6 22 | 23 | # the idle timeout of a connection in milliseconds for keep-alive 24 | idleTimeoutMillis: 20000 25 | 26 | # maximum thread pool size to execute internal potential long running tasks 27 | maxWorkerThreadPoolSize: 11 28 | 29 | # the configuration options for every underlying TCP socket 30 | channelOptions: 31 | SO_BACKLOG: 1024 32 | SO_RCVBUF: 1024 33 | SO_SNDBUF: 1024 34 | TCP_NODELAY: False 35 | 36 | # the interval for the purge thread to scan all the filters to find and clean expired filters 37 | purgeFilterIntervalMillis: 200 38 | 39 | # config when to save all the filters on disk. Will save the filters if both the given number of seconds and the given 40 | # number of update operations against the service occurred. 41 | # In the example below the behaviour will be to save: 42 | # after 900 sec if at least 1 filter update operation occurred 43 | # after 300 sec if at least 10 filter update operation occurred 44 | # after 60 sec if at least 10000 filter update operation occurred 45 | triggerPersistenceCriteria: 46 | - periodInSeconds: 901 47 | updatesMoreThan: 2 48 | - periodInSeconds: 301 49 | updatesMoreThan: 11 50 | - periodInSeconds: 61 51 | updatesMoreThan: 10001 52 | 53 | # the path to a directory to store persistent file. Leave it empty to use the path to "user.dir" system property 54 | persistentStorageDirectory: "./log/storage" 55 | 56 | # 100KB 57 | channelBufferSizeForFilterPersistence: 102401 58 | 59 | # when this switch is on, we try to recover filters from a corrupted or unfinished persistent file as many filters as we can; 60 | # otherwise, we will throw an exception when we make sure that the persistent file is corrupted or unfinished. 61 | allowRecoverFromCorruptedPersistentFile: True 62 | 63 | # the number of milliseconds to wait for active requests to go end before shutting down. 0 means the server 64 | # will stop right away without waiting 65 | gracefulShutdownQuietPeriodMillis: 1 66 | 67 | # the number of milliseconds to wait before shutting down the server regardless of active requests. 68 | # This should be set to a time greater than gracefulShutdownQuietPeriodMillis to ensure the server 69 | # shuts down even if there is a stuck request. 70 | gracefulShutdownTimeoutMillis: 1 71 | -------------------------------------------------------------------------------- /filter-service-metrics/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | filter-service 5 | cn.leancloud 6 | 1.16-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | filter-service-metrics 11 | jar 12 | filter-service-metrics ${project.version} 13 | 14 | 15 | 16 | 17 | io.micrometer 18 | micrometer-core 19 | 20 | 21 | com.google.j2objc 22 | j2objc-annotations 23 | 24 | 25 | org.yaml 26 | snakeyaml 27 | 28 | 29 | commons-logging 30 | commons-logging 31 | 32 | 33 | org.codehaus.mojo 34 | animal-sniffer-annotations 35 | 36 | 37 | com.google.errorprone 38 | error_prone_annotations 39 | 40 | 41 | 42 | 43 | com.linecorp.armeria 44 | armeria 45 | 46 | 47 | 48 | 49 | 50 | org.sonatype.plugins 51 | nexus-staging-maven-plugin 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /filter-service-metrics/src/main/java/cn/leancloud/filter/service/metrics/MetricsService.java: -------------------------------------------------------------------------------- 1 | package cn.leancloud.filter.service.metrics; 2 | 3 | import io.micrometer.core.instrument.MeterRegistry; 4 | 5 | /** 6 | * This is a SPI interface to create a {@link MeterRegistry} to generate metrics from filter-service. 7 | * You can implement this interface as your requirements and provide the implementation 8 | * under {@code META-INF/services}. Then filter-service will load your implementation 9 | * by using {@link java.util.ServiceLoader}. 10 | */ 11 | public interface MetricsService { 12 | /** 13 | * A start hook called before filter-service start. 14 | */ 15 | default void start() throws Exception {} 16 | 17 | /** 18 | * Create a new {@link MeterRegistry} used by filter-service to generate metrics. 19 | * @return a custom {@link MeterRegistry} which meet your requirements 20 | */ 21 | MeterRegistry createMeterRegistry() throws Exception; 22 | 23 | /** 24 | * A stop hook called after filter-service is stopped. 25 | */ 26 | default void stop() throws Exception {} 27 | } 28 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | --------------------------------------------------------------------------------