diff --git a/.circleci/config.yml b/.circleci/config.yml index eb7c6d17809a..1f9d398ee1ce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -80,7 +80,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -113,10 +113,10 @@ jobs: j17_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -189,7 +189,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -224,7 +224,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -308,7 +308,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -375,7 +375,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -410,7 +410,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -466,7 +466,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -499,10 +499,10 @@ jobs: j11_dtests_latest_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -601,7 +601,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -668,7 +668,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -704,7 +704,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -788,7 +788,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -820,10 +820,10 @@ jobs: j17_cqlsh_dtests_py38_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -898,7 +898,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -930,10 +930,10 @@ jobs: j17_dtests_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1054,7 +1054,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1086,10 +1086,10 @@ jobs: j11_dtests_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1188,7 +1188,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1221,10 +1221,10 @@ jobs: j17_cqlsh_dtests_py311_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1299,7 +1299,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1334,7 +1334,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1418,7 +1418,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1454,7 +1454,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1510,7 +1510,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1546,7 +1546,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1630,7 +1630,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1662,10 +1662,10 @@ jobs: j11_cqlsh_dtests_py38_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1740,7 +1740,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1776,7 +1776,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1832,7 +1832,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1864,10 +1864,10 @@ jobs: j11_cqlsh_dtests_py311: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1942,7 +1942,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -1975,10 +1975,10 @@ jobs: j17_dtests_large_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2077,7 +2077,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2109,10 +2109,10 @@ jobs: j17_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2233,7 +2233,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2268,7 +2268,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2352,7 +2352,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2388,7 +2388,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2444,7 +2444,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2476,10 +2476,10 @@ jobs: j17_cqlsh_dtests_py311: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2554,7 +2554,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2586,10 +2586,10 @@ jobs: j11_cqlsh_dtests_py38: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2664,7 +2664,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2700,7 +2700,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2756,7 +2756,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2829,7 +2829,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2865,7 +2865,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2921,7 +2921,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -2957,7 +2957,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3041,7 +3041,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3076,7 +3076,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3239,7 +3239,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3272,10 +3272,10 @@ jobs: j11_dtests_large_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3326,7 +3326,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3362,7 +3362,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3418,7 +3418,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3450,10 +3450,10 @@ jobs: j11_dtests_large_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3552,7 +3552,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3585,10 +3585,10 @@ jobs: j11_dtests_large: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3639,7 +3639,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3675,7 +3675,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3731,7 +3731,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3767,7 +3767,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3851,7 +3851,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -3884,10 +3884,10 @@ jobs: j11_upgrade_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3986,7 +3986,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4022,7 +4022,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4106,7 +4106,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4141,7 +4141,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4304,7 +4304,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4336,7 +4336,7 @@ jobs: j17_build: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 1 @@ -4399,6 +4399,7 @@ jobs: - .m2 environment: - ANT_HOME: /usr/share/ant + - ANT_OPTS: "-Xmx3G" - LANG: en_US.UTF-8 - KEEP_TEST_DIR: true - DEFAULT_DIR: /home/cassandra/cassandra-dtest @@ -4412,7 +4413,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4447,7 +4448,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4531,7 +4532,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4567,7 +4568,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4623,7 +4624,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4658,7 +4659,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4742,7 +4743,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4815,7 +4816,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4851,7 +4852,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4907,7 +4908,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -4943,7 +4944,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4999,7 +5000,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5104,7 +5105,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5178,7 +5179,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5214,7 +5215,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5270,7 +5271,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5385,7 +5386,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5421,7 +5422,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5505,7 +5506,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5579,7 +5580,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5615,7 +5616,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5671,7 +5672,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5704,10 +5705,10 @@ jobs: j17_dtests_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5780,7 +5781,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5812,10 +5813,10 @@ jobs: j11_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5914,7 +5915,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -5950,7 +5951,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6034,7 +6035,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6070,7 +6071,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6154,7 +6155,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6187,10 +6188,10 @@ jobs: j17_dtests_latest_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6289,7 +6290,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6324,7 +6325,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6380,7 +6381,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6413,10 +6414,10 @@ jobs: j11_upgrade_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6467,7 +6468,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6500,10 +6501,10 @@ jobs: j11_dtests_large_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6602,7 +6603,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6638,7 +6639,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6694,7 +6695,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6767,7 +6768,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6799,10 +6800,10 @@ jobs: j17_dtests_large_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6853,7 +6854,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6885,10 +6886,10 @@ jobs: j11_cqlsh_dtests_py38_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6963,7 +6964,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -6999,7 +7000,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7055,7 +7056,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7091,7 +7092,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7147,7 +7148,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7180,10 +7181,10 @@ jobs: j11_cqlsh_dtests_py311_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7258,7 +7259,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7324,7 +7325,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7356,10 +7357,10 @@ jobs: j11_cqlsh_dtests_py311_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7434,7 +7435,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7470,7 +7471,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7554,7 +7555,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7586,10 +7587,10 @@ jobs: j17_cqlsh_dtests_py38_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7664,7 +7665,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7696,10 +7697,10 @@ jobs: j17_cqlsh_dtests_py311_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7774,7 +7775,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7809,7 +7810,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7893,7 +7894,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -7926,10 +7927,10 @@ jobs: j17_dtests_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8002,7 +8003,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8037,7 +8038,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8121,7 +8122,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8194,7 +8195,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8226,7 +8227,7 @@ jobs: j11_build: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 1 @@ -8289,6 +8290,7 @@ jobs: - .m2 environment: - ANT_HOME: /usr/share/ant + - ANT_OPTS: "-Xmx3G" - LANG: en_US.UTF-8 - KEEP_TEST_DIR: true - DEFAULT_DIR: /home/cassandra/cassandra-dtest @@ -8302,7 +8304,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8338,7 +8340,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8394,7 +8396,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8427,10 +8429,10 @@ jobs: j11_dtests_latest: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8481,7 +8483,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8547,7 +8549,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8580,10 +8582,10 @@ jobs: j11_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8634,7 +8636,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8667,10 +8669,10 @@ jobs: j17_cqlsh_dtests_py38: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8745,7 +8747,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8780,7 +8782,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8864,7 +8866,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -8899,7 +8901,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8955,7 +8957,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9029,7 +9031,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9064,7 +9066,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9120,7 +9122,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9152,10 +9154,10 @@ jobs: j17_dtests_large: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9206,7 +9208,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9241,7 +9243,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9297,7 +9299,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9371,7 +9373,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9406,7 +9408,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9462,7 +9464,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9494,10 +9496,10 @@ jobs: j11_dtests_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9548,7 +9550,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9584,7 +9586,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9640,7 +9642,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9672,10 +9674,10 @@ jobs: j17_dtests_large_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9774,7 +9776,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9809,7 +9811,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9865,7 +9867,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -9900,7 +9902,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9956,7 +9958,7 @@ jobs: - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false - - REPEATED_UTESTS: null + - REPEATED_UTESTS: org.apache.cassandra.schema.SystemDistributedKeyspaceCompressionDictionaryTest,org.apache.cassandra.io.compress.ZstdDictionaryCompressorTest,org.apache.cassandra.io.compress.ZstdCompressorTest,org.apache.cassandra.db.compression.CompressionDictionaryCacheTest - REPEATED_UTESTS_COUNT: 500 - REPEATED_UTESTS_FQLTOOL: null - REPEATED_UTESTS_FQLTOOL_COUNT: 500 @@ -10009,6 +10011,17 @@ workflows: - success j11_build: - success + - start_j11_unit_tests_repeat: + type: approval + - j11_unit_tests_repeat: + requires: + - start_j11_unit_tests_repeat + - j11_build + upstream: + start_j11_unit_tests_repeat: + - success + j11_build: + - success - start_j11_jvm_dtests: type: approval - j11_jvm_dtests: @@ -10119,6 +10132,17 @@ workflows: - success j11_build: - success + - start_j17_unit_tests_repeat: + type: approval + - j17_unit_tests_repeat: + requires: + - start_j17_unit_tests_repeat + - j11_build + upstream: + start_j17_unit_tests_repeat: + - success + j11_build: + - success - start_j11_utests_oa: type: approval - j11_utests_oa: @@ -10130,6 +10154,17 @@ workflows: - success j11_build: - success + - start_j11_utests_oa_repeat: + type: approval + - j11_utests_oa_repeat: + requires: + - start_j11_utests_oa_repeat + - j11_build + upstream: + start_j11_utests_oa_repeat: + - success + j11_build: + - success - start_j17_utests_oa: type: approval - j17_utests_oa: @@ -10141,6 +10176,17 @@ workflows: - success j11_build: - success + - start_j17_utests_oa_repeat: + type: approval + - j17_utests_oa_repeat: + requires: + - start_j17_utests_oa_repeat + - j11_build + upstream: + start_j17_utests_oa_repeat: + - success + j11_build: + - success - start_j11_utests_long: type: approval - j11_utests_long: @@ -10185,6 +10231,28 @@ workflows: - success j11_build: - success + - start_j11_utests_cdc_repeat: + type: approval + - j11_utests_cdc_repeat: + requires: + - start_j11_utests_cdc_repeat + - j11_build + upstream: + start_j11_utests_cdc_repeat: + - success + j11_build: + - success + - start_j17_utests_cdc_repeat: + type: approval + - j17_utests_cdc_repeat: + requires: + - start_j17_utests_cdc_repeat + - j11_build + upstream: + start_j17_utests_cdc_repeat: + - success + j11_build: + - success - start_j11_utests_compression: type: approval - j11_utests_compression: @@ -10207,6 +10275,28 @@ workflows: - success j11_build: - success + - start_j11_utests_compression_repeat: + type: approval + - j11_utests_compression_repeat: + requires: + - start_j11_utests_compression_repeat + - j11_build + upstream: + start_j11_utests_compression_repeat: + - success + j11_build: + - success + - start_j17_utests_compression_repeat: + type: approval + - j17_utests_compression_repeat: + requires: + - start_j17_utests_compression_repeat + - j11_build + upstream: + start_j17_utests_compression_repeat: + - success + j11_build: + - success - start_j11_utests_latest: type: approval - j11_utests_latest: @@ -10229,6 +10319,28 @@ workflows: - success j11_build: - success + - start_j11_utests_latest_repeat: + type: approval + - j11_utests_latest_repeat: + requires: + - start_j11_utests_latest_repeat + - j11_build + upstream: + start_j11_utests_latest_repeat: + - success + j11_build: + - success + - start_j17_utests_latest_repeat: + type: approval + - j17_utests_latest_repeat: + requires: + - start_j17_utests_latest_repeat + - j11_build + upstream: + start_j17_utests_latest_repeat: + - success + j11_build: + - success - start_j11_utests_stress: type: approval - j11_utests_stress: @@ -10317,6 +10429,28 @@ workflows: - success j11_build: - success + - start_j11_utests_system_keyspace_directory_repeat: + type: approval + - j11_utests_system_keyspace_directory_repeat: + requires: + - start_j11_utests_system_keyspace_directory_repeat + - j11_build + upstream: + start_j11_utests_system_keyspace_directory_repeat: + - success + j11_build: + - success + - start_j17_utests_system_keyspace_directory_repeat: + type: approval + - j17_utests_system_keyspace_directory_repeat: + requires: + - start_j17_utests_system_keyspace_directory_repeat + - j11_build + upstream: + start_j17_utests_system_keyspace_directory_repeat: + - success + j11_build: + - success - start_j11_dtest_jars_build: type: approval - j11_dtest_jars_build: @@ -10598,12 +10732,36 @@ workflows: upstream: j11_build: - success + - j11_utests_oa_repeat: + requires: + - j11_build + upstream: + j11_build: + - success + - j17_utests_oa_repeat: + requires: + - j11_build + upstream: + j11_build: + - success + - j11_unit_tests_repeat: + requires: + - j11_build + upstream: + j11_build: + - success - j11_utests_latest: requires: - j11_build upstream: j11_build: - success + - j11_utests_latest_repeat: + requires: + - j11_build + upstream: + j11_build: + - success - j11_simulator_dtests: requires: - j11_build @@ -10670,12 +10828,24 @@ workflows: upstream: j11_build: - success + - j17_unit_tests_repeat: + requires: + - j11_build + upstream: + j11_build: + - success - j17_utests_latest: requires: - j11_build upstream: j11_build: - success + - j17_utests_latest_repeat: + requires: + - j11_build + upstream: + j11_build: + - success - start_utests_long: type: approval - j11_utests_long: @@ -10716,6 +10886,24 @@ workflows: - success j11_build: - success + - j11_utests_cdc_repeat: + requires: + - start_utests_cdc + - j11_build + upstream: + start_utests_cdc: + - success + j11_build: + - success + - j17_utests_cdc_repeat: + requires: + - start_utests_cdc + - j11_build + upstream: + start_utests_cdc: + - success + j11_build: + - success - start_utests_compression: type: approval - j11_utests_compression: @@ -10736,6 +10924,24 @@ workflows: - success j11_build: - success + - j11_utests_compression_repeat: + requires: + - start_utests_compression + - j11_build + upstream: + start_utests_compression: + - success + j11_build: + - success + - j17_utests_compression_repeat: + requires: + - start_utests_compression + - j11_build + upstream: + start_utests_compression: + - success + j11_build: + - success - start_utests_stress: type: approval - j11_utests_stress: @@ -10813,6 +11019,21 @@ workflows: - success j11_build: - success + - j11_utests_system_keyspace_directory_repeat: + requires: + - j11_build + upstream: + j11_build: + - success + - j17_utests_system_keyspace_directory_repeat: + requires: + - start_utests_system_keyspace_directory + - j11_build + upstream: + start_utests_system_keyspace_directory: + - success + j11_build: + - success - start_jvm_upgrade_dtests: type: approval - j11_dtest_jars_build: @@ -11026,6 +11247,17 @@ workflows: - success j17_build: - success + - start_j17_unit_tests_repeat: + type: approval + - j17_unit_tests_repeat: + requires: + - start_j17_unit_tests_repeat + - j17_build + upstream: + start_j17_unit_tests_repeat: + - success + j17_build: + - success - start_j17_jvm_dtests: type: approval - j17_jvm_dtests: @@ -11194,6 +11426,17 @@ workflows: - success j17_build: - success + - start_j17_utests_oa_repeat: + type: approval + - j17_utests_oa_repeat: + requires: + - start_j17_utests_oa_repeat + - j17_build + upstream: + start_j17_utests_oa_repeat: + - success + j17_build: + - success - start_j17_utests_long: type: approval - j17_utests_long: @@ -11216,6 +11459,17 @@ workflows: - success j17_build: - success + - start_j17_utests_cdc_repeat: + type: approval + - j17_utests_cdc_repeat: + requires: + - start_j17_utests_cdc_repeat + - j17_build + upstream: + start_j17_utests_cdc_repeat: + - success + j17_build: + - success - start_j17_utests_compression: type: approval - j17_utests_compression: @@ -11227,6 +11481,17 @@ workflows: - success j17_build: - success + - start_j17_utests_compression_repeat: + type: approval + - j17_utests_compression_repeat: + requires: + - start_j17_utests_compression_repeat + - j17_build + upstream: + start_j17_utests_compression_repeat: + - success + j17_build: + - success - start_j17_utests_latest: type: approval - j17_utests_latest: @@ -11238,6 +11503,17 @@ workflows: - success j17_build: - success + - start_j17_utests_latest_repeat: + type: approval + - j17_utests_latest_repeat: + requires: + - start_j17_utests_latest_repeat + - j17_build + upstream: + start_j17_utests_latest_repeat: + - success + j17_build: + - success - start_j17_utests_stress: type: approval - j17_utests_stress: @@ -11282,6 +11558,17 @@ workflows: - success j17_build: - success + - start_j17_utests_system_keyspace_directory_repeat: + type: approval + - j17_utests_system_keyspace_directory_repeat: + requires: + - start_j17_utests_system_keyspace_directory_repeat + - j17_build + upstream: + start_j17_utests_system_keyspace_directory_repeat: + - success + j17_build: + - success java17_pre-commit_tests: jobs: - start_pre-commit_tests: @@ -11304,12 +11591,30 @@ workflows: upstream: j17_build: - success + - j17_utests_oa_repeat: + requires: + - j17_build + upstream: + j17_build: + - success + - j17_unit_tests_repeat: + requires: + - j17_build + upstream: + j17_build: + - success - j17_utests_latest: requires: - j17_build upstream: j17_build: - success + - j17_utests_latest_repeat: + requires: + - j17_build + upstream: + j17_build: + - success - j17_jvm_dtests: requires: - j17_build @@ -11438,6 +11743,15 @@ workflows: - success j17_build: - success + - j17_utests_cdc_repeat: + requires: + - start_utests_cdc + - j17_build + upstream: + start_utests_cdc: + - success + j17_build: + - success - start_utests_compression: type: approval - j17_utests_compression: @@ -11449,6 +11763,15 @@ workflows: - success j17_build: - success + - j17_utests_compression_repeat: + requires: + - start_utests_compression + - j17_build + upstream: + start_utests_compression: + - success + j17_build: + - success - start_utests_stress: type: approval - j17_utests_stress: @@ -11493,3 +11816,12 @@ workflows: - success j17_build: - success + - j17_utests_system_keyspace_directory_repeat: + requires: + - start_utests_system_keyspace_directory + - j17_build + upstream: + start_utests_system_keyspace_directory: + - success + j17_build: + - success diff --git a/CHANGES.txt b/CHANGES.txt index e75ae3276f3a..91644f89419a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,8 @@ 6.0-alpha3 + * Fix Compression dictionary distribution silently leaving a node on a stale dictionary (CASSANDRA-21681) + * ZstdCompressionDictionary reference leak when opening dictionary-compressed SSTables concurrently (CASSANDRA-21680) + * Avoid ZstdDictionary to allocate a native zstd context per chunk (CASSANDRA-21677) + * Avoid ZstdDictionaryCompressor to allocate a native zstd context per chunk (CASSANDRA-21672) * Fix non-printable characters in Gossiper log for TOKENS (CASSANDRA-21417) * Fix compression dictionary training failing with "insufficient samples" on large-chunk tables (CASSANDRA-21666) * AccordExecutor improvements (CASSANDRA-21662) diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 87eb79f53a66..9ae4c182015d 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -3029,3 +3029,18 @@ compression_dictionary_cache_size: 10 # Expired dictionaries will be removed from memory but can be reloaded if needed. # Min unit: s compression_dictionary_cache_expire: 24h + +# Controls if auto-training of compression dictionaries is enabled on this node or not +# A node has to be a full CMS member with the lowest id to be selected for auto-training. +# There is always at most one node who auto-trains in the whole cluster. +compression_dictionary_auto_training_enabled: false + +# Controls the interval auto-training will be conducted at. Each interval, training will be +# conducted all tables which have auto_training_enabled set to true in their compression parameters +# when a dictionary compressor is in use. +compression_dictionary_auto_training_interval: 1h + +# Controls initial delay of auto-training. If training interval is, for example, 1 day, +# we would start auto-training for the first time after one day. If this is set e.g. to 1 hour, +# Then the initial auto-training will be done after 1 hour and each next one will be done 1 day after it. +compression_dictionary_auto_training_initial_delay: 1h diff --git a/conf/cassandra_latest.yaml b/conf/cassandra_latest.yaml index 732e1f2dee29..7d2a700e00d9 100644 --- a/conf/cassandra_latest.yaml +++ b/conf/cassandra_latest.yaml @@ -2780,3 +2780,18 @@ compression_dictionary_cache_size: 10 # Expired dictionaries will be removed from memory but can be reloaded if needed. # Min unit: s compression_dictionary_cache_expire: 24h + +# Controls if auto-training of compression dictionaries is enabled on this node or not +# A node has to be a full CMS member with the lowest id to be selected for auto-training. +# There is always at most one node who auto-trains in the whole cluster. +compression_dictionary_auto_training_enabled: false + +# Controls the interval auto-training will be conducted at. Each interval, training will be +# conducted all tables which have auto_training_enabled set to true in their compression parameters +# when a dictionary compressor is in use. +compression_dictionary_auto_training_interval: 1h + +# Controls initial delay of auto-training. If training interval is, for example, 1 day, +# we would start auto-training for the first time after one day. If this is set e.g. to 1 hour, +# Then the initial auto-training will be done after 1 hour and each next one will be done 1 day after it. +compression_dictionary_auto_training_initial_delay: 1h \ No newline at end of file diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index f6853e0baeba..9e5b99b05bba 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -578,6 +578,9 @@ public static class SSTableConfig public volatile DurationSpec.IntSecondsBound compression_dictionary_refresh_initial_delay = new DurationSpec.IntSecondsBound("10s"); // 10 seconds default public volatile int compression_dictionary_cache_size = 10; // max dictionaries per table public volatile DurationSpec.IntSecondsBound compression_dictionary_cache_expire = new DurationSpec.IntSecondsBound("24h"); + public volatile boolean compression_dictionary_auto_training_enabled = false; + public volatile DurationSpec.IntMinutesBound compression_dictionary_auto_training_interval = new DurationSpec.IntMinutesBound("1h"); + public volatile DurationSpec.IntMinutesBound compression_dictionary_auto_training_initial_delay = new DurationSpec.IntMinutesBound("1h"); public DataStorageSpec.LongMebibytesBound paxos_cache_size = null; diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index b51c23499664..b8a693742e8b 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -1280,6 +1280,15 @@ else if (conf.max_value_size.toMebibytes() >= 2048) { throw new ConfigurationException(ex.getMessage()); } + + // The auto-training scheduler uses this as the fixed delay between check cycles, so it must be strictly + // positive: ScheduledExecutorService.scheduleWithFixedDelay rejects a non-positive period. A negative value + // is already rejected by DurationSpec at parse time; this additionally rejects zero. The initial delay may be + // zero (run immediately) and the enabled flag is a boolean, so neither needs validation here. + if (conf.compression_dictionary_auto_training_interval == null || + conf.compression_dictionary_auto_training_interval.toSeconds() <= 0) + throw new ConfigurationException("compression_dictionary_auto_training_interval must be positive, but was " + + conf.compression_dictionary_auto_training_interval, false); } @VisibleForTesting @@ -4645,6 +4654,21 @@ public static int getCompressionDictionaryCacheExpireSeconds() return conf.compression_dictionary_cache_expire.toSeconds(); } + public static boolean getCompressionDictionaryAutoTrainingEnabled() + { + return conf.compression_dictionary_auto_training_enabled; + } + + public static int getCompressionDictionaryAutoTrainingInterval() + { + return conf.compression_dictionary_auto_training_interval.toSeconds(); + } + + public static int getCompressionDictionaryAutoTrainingInitialDelay() + { + return conf.compression_dictionary_auto_training_initial_delay.toSeconds(); + } + public static int getStreamingKeepAlivePeriod() { return conf.streaming_keep_alive_period.toSeconds(); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java index a75501ed120f..09daed69c650 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java @@ -51,6 +51,7 @@ import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.messages.ResultMessage; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.AUTO_TRAINING_ENABLED; import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_TRAINING_MIN_FREQUENCY; import static org.apache.cassandra.io.compress.IDictionaryCompressor.TRAINING_MIN_FREQUENCY_PARAMETER_NAME; import static org.apache.cassandra.schema.KeyspaceMetadata.validateKeyspaceName; @@ -249,6 +250,26 @@ protected void validateMinimumTrainingFrequencyForDictionaryCompressor(TablePara } } + /** + * Compression dictionary auto-training is only supported on tables using {@link TimeWindowCompactionStrategy} + * for now: the auto-trainer biases its training sample toward the newest time window to detect data drift. + * Reject enabling {@code auto_training_enabled} on a table that uses any other compaction strategy. + */ + protected void validateCompactionStrategySupportsAutoTraining(TableParams params) + { + if (!SchemaConstants.isSystemKeyspace(keyspaceName) && + params.compression.isDictionaryCompressionEnabled() && + Boolean.parseBoolean(params.compression.getOtherOptions().getOrDefault(AUTO_TRAINING_ENABLED, "false")) && + !TimeWindowCompactionStrategy.class.isAssignableFrom(params.compaction.klass())) + { + throw ire("Compression dictionary auto-training ('%s') is only supported on tables using %s, " + + "but %s is configured for this table", + AUTO_TRAINING_ENABLED, + TimeWindowCompactionStrategy.class.getSimpleName(), + params.compaction.klass().getSimpleName()); + } + } + private void grantPermissionsOnResource(IResource resource, AuthenticatedUser user) { try diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index 09501c844655..fb2090f5ba29 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -688,6 +688,10 @@ public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetad TableParams params = attrs.asAlteredTableParams(table.params); + // Validate against the altered (merged) params, not the validate()-time defaults, so we see the + // table's actual compaction strategy when only compression (e.g. auto_training) is being changed. + validateCompactionStrategySupportsAutoTraining(params); + if (table.isCounter() && params.defaultTimeToLive > 0) throw ire("Cannot set default_time_to_live on a table with counters"); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java index 1250cb79ec6b..ba3e11f92ddc 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java @@ -222,6 +222,11 @@ public Keyspaces apply(ClusterMetadata metadata) TableParams originalParams = targetBuilder.build().params; TableParams newTableParams = attrs.asAlteredTableParams(originalParams); + // Validate against the merged params: compression may be inherited from the source table while the WITH + // clause overrides compaction, or vice versa, so neither side alone tells us whether auto-training is + // being paired with a non-TWCS strategy. + validateCompactionStrategySupportsAutoTraining(newTableParams); + TableMetadata table = targetBuilder.params(newTableParams) .id(TableId.get(metadata)) .build(); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java index 26dfbb8dec76..f713d1d994e3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -230,6 +230,7 @@ public void validate(ClientState state) TableParams paramsForValidation = attrs.asNewTableParams(keyspaceName); validateDefaultTimeToLive(paramsForValidation); validateMinimumTrainingFrequencyForDictionaryCompressor(paramsForValidation); + validateCompactionStrategySupportsAutoTraining(paramsForValidation); rawColumns.forEach((name, raw) -> raw.validate(state, name)); } diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java index 50e962ac04f0..be802ea71a91 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java @@ -537,6 +537,12 @@ public ColumnFamilyStore(Keyspace keyspace, data.subscribe(StorageService.instance.sstablesTracker); data.subscribe(SnapshotManager.instance); + // Initialize the compression dictionary manager BEFORE openAll() below. Loading dictionary-compressed + // sstables resolves each dictionary through owner.compressionDictionaryManager(); if that is still null + // here, CompressionMetadata opens an uncached dictionary whose lazily-created selfRef is owned by no one + // and never released — leaking one dictionary reference per dict sstable opened at startup (CASSANDRA-21047). + compressionDictionaryManager = new CompressionDictionaryManager(this, registerBookeeping); + Collection sstables = null; // scan for sstables corresponding to this cf and load them if (data.loadsstables) @@ -592,7 +598,6 @@ public ColumnFamilyStore(Keyspace keyspace, streamManager = new CassandraStreamManager(this); repairManager = new CassandraTableRepairManager(this); sstableImporter = new SSTableImporter(this); - compressionDictionaryManager = new CompressionDictionaryManager(this, registerBookeeping); if (DatabaseDescriptor.isClientOrToolInitialized() || SchemaConstants.isSystemKeyspace(getKeyspaceName())) topPartitions = null; diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionary.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionary.java index b947bb3a19cb..cd83fbd5e2ce 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionary.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionary.java @@ -254,10 +254,17 @@ static CompressionDictionary deserialize(DataInput input, @Nullable CompressionD CompressionDictionary dictionary = kind.createDictionary(dictId, dict, checksum); - // update the dictionary manager if it exists + // Register with the manager and adopt the CANONICAL cached instance it returns. Under concurrent + // opens (e.g. the multi-threaded SSTableBatchOpen pool) two threads can both miss the cache and + // create separate instances for the same dictId; only one wins the cache. Returning our own + // (possibly losing) instance here would let the caller tryRef() an uncached dictionary whose + // lazily-created selfRef the cache never owns or releases, leaking it once the referrer is + // garbage collected (CASSANDRA-21047). if (manager != null) { - manager.add(dictionary); + CompressionDictionary canonical = manager.add(dictionary); + if (canonical != null) + dictionary = canonical; } return dictionary; @@ -391,7 +398,10 @@ public CompressionDictionary createDictionary(CompressionDictionary.DictId dictI * @param createdAt creation date of to-be-constructed dictionary * @return a compression dictionary instance */ - public abstract CompressionDictionary createDictionary(CompressionDictionary.DictId dictId, byte[] dict, int checksum, Instant createdAt); + public abstract CompressionDictionary createDictionary(CompressionDictionary.DictId dictId, + byte[] dict, + int checksum, + Instant createdAt); /** * Creates a dictionary compressor for this kind diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingHistory.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingHistory.java new file mode 100644 index 000000000000..2ef3aa4e5491 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingHistory.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.FBUtilities; + +/** + * Node-local record of the auto-training adoption decisions this node has made, exposed by the + * {@code system_views.compression_dictionary_auto_training} virtual table. + *

+ * Auto-training evaluates a freshly trained candidate dictionary against the table's current one and adopts the + * candidate only if it improves the compression ratio by at least the configured threshold. That decision is + * otherwise only visible in the log, which makes it awkward to answer "has this table ever produced a better + * dictionary, and by how much". Each decision is kept here instead. + *

+ * The history is held in memory only: it is bounded, and a restart starts afresh. Only the node that ran the + * training records anything, so a cluster-wide picture means querying every node. + */ +public class CompressionDictionaryAutoTrainingHistory +{ + public static final int DEFAULT_MAX_ENTRIES = 1000; + + public static final CompressionDictionaryAutoTrainingHistory instance = + new CompressionDictionaryAutoTrainingHistory(DEFAULT_MAX_ENTRIES); + + private final int maxEntries; + // newest first, so the virtual table and the eviction of the oldest entry both read naturally + private final Deque entries = new ArrayDeque<>(); + + @VisibleForTesting + public CompressionDictionaryAutoTrainingHistory(int maxEntries) + { + this.maxEntries = maxEntries; + } + + /** + * Records one adoption decision. + * + * @param keyspaceName keyspace the candidate was trained for + * @param tableName table the candidate was trained for + * @param kind kind of the dictionary that was trained + * @param baselineRatio compressed/uncompressed ratio the current dictionary achieved on the sample + * @param candidateRatio compressed/uncompressed ratio the candidate achieved on the same sample + * @param improvement relative improvement of the candidate over the baseline + * @param threshold improvement the candidate had to reach to be adopted + * @param promoted whether the candidate became the table's dictionary + */ + public void record(String keyspaceName, + String tableName, + CompressionDictionary.Kind kind, + double baselineRatio, + double candidateRatio, + double improvement, + double threshold, + boolean promoted) + { + record(FBUtilities.now().toEpochMilli(), keyspaceName, tableName, kind, + baselineRatio, candidateRatio, improvement, threshold, promoted); + } + + /** + * Records a decision at an explicit time. A table trains one candidate at a time and training is far slower + * than the millisecond resolution of the timestamp, so in production two decisions for one table cannot share + * a timestamp; tests that need several entries have to space them out themselves. + */ + @VisibleForTesting + public void record(long timestampMillis, + String keyspaceName, + String tableName, + CompressionDictionary.Kind kind, + double baselineRatio, + double candidateRatio, + double improvement, + double threshold, + boolean promoted) + { + Entry entry = new Entry(timestampMillis, + FBUtilities.getBroadcastAddressAndPort(), + keyspaceName, + tableName, + kind, + baselineRatio, + candidateRatio, + improvement, + threshold, + promoted); + + synchronized (entries) + { + if (entries.size() == maxEntries) + entries.removeLast(); + + entries.addFirst(entry); + } + } + + /** + * @return the recorded decisions, newest first + */ + public List entries() + { + synchronized (entries) + { + return new ArrayList<>(entries); + } + } + + @VisibleForTesting + public void clear() + { + synchronized (entries) + { + entries.clear(); + } + } + + public static class Entry + { + public final long timestampMillis; + public final InetAddressAndPort node; + public final String keyspaceName; + public final String tableName; + public final CompressionDictionary.Kind kind; + public final double baselineRatio; + public final double candidateRatio; + public final double improvement; + public final double threshold; + public final boolean promoted; + + Entry(long timestampMillis, + InetAddressAndPort node, + String keyspaceName, + String tableName, + CompressionDictionary.Kind kind, + double baselineRatio, + double candidateRatio, + double improvement, + double threshold, + boolean promoted) + { + this.timestampMillis = timestampMillis; + this.node = node; + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.kind = kind; + this.baselineRatio = baselineRatio; + this.candidateRatio = candidateRatio; + this.improvement = improvement; + this.threshold = threshold; + this.promoted = promoted; + } + } +} diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManager.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManager.java new file mode 100644 index 000000000000..b699accd01c8 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManager.java @@ -0,0 +1,557 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import com.google.common.annotations.VisibleForTesting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ScheduledExecutorPlus; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.compress.IDictionaryCompressor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SystemDistributedKeyspace; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.utils.ExecutorUtils; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.Refs; + +/** + * Singleton that periodically retrains compression dictionaries for dictionary-compressed tables and + * adopts a freshly trained dictionary only when it compresses better than the current one. + *

+ * The scheduled task runs on every node, but only the first CMS member (by lowest + * {@link NodeId}) performs the actual check on each cycle. This allows leadership + * to migrate naturally as CMS membership changes. + *

+ * On each check cycle, iterates over all live {@link ColumnFamilyStore} instances via + * {@code ColumnFamilyStore.all()}, skipping tables that are not eligible (see + * {@link #isEligibleTable(ColumnFamilyStore)}), that do not have dictionary compression enabled, + * or that do not have {@code auto_training_enabled = true} in their compression options. + *

+ * For an eligible table, a candidate dictionary is trained from a fresh sample of the current + * SSTables. The candidate is then compared against the current dictionary on a second sample drawn + * independently from the same SSTables: both dictionaries compress the same chunks, and the candidate + * is adopted only if it improves the compression ratio by at least + * {@link IDictionaryCompressor#DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE}. + * Note that the second sample is re-drawn, not held out: nothing excludes the chunks the candidate was + * trained on, and when the sample budget covers every chunk the two samples coincide. The comparison is + * therefore partly in-sample and to that extent flatters the candidate. + *

+ * No per-table state is cached in memory. The current dictionary and its last training time are read + * from the {@code system_distributed.compression_dictionaries} table on every check cycle, ensuring + * correctness after manual training or node restarts. + */ +public class CompressionDictionaryAutoTrainingManager implements AutoCloseable +{ + private static final Logger logger = LoggerFactory.getLogger(CompressionDictionaryAutoTrainingManager.class); + + public static final CompressionDictionaryAutoTrainingManager instance = new CompressionDictionaryAutoTrainingManager(); + + private volatile ScheduledExecutorPlus executor; + private volatile ScheduledFuture checkTask; + private volatile boolean closed; + + @VisibleForTesting + CompressionDictionaryAutoTrainingManager() + { + } + + /** + * Starts the periodic compression ratio check on this node. + * The task runs on every node but only performs work on the first CMS member. + */ + public void start() + { + if (!DatabaseDescriptor.getCompressionDictionaryAutoTrainingEnabled()) + { + logger.debug("Dictionary auto training management is disabled."); + return; + } + + int initialDelay = DatabaseDescriptor.getCompressionDictionaryAutoTrainingInitialDelay(); + int interval = DatabaseDescriptor.getCompressionDictionaryAutoTrainingInterval(); + start(initialDelay, interval, TimeUnit.SECONDS); + } + + @VisibleForTesting + synchronized void start(long initialDelay, long interval, TimeUnit unit) + { + if (checkTask != null) + return; + + closed = false; + executor = ExecutorFactory.Global.executorFactory().scheduled(false, "CompressionDictionaryAutoTraining"); + checkTask = executor.scheduleWithFixedDelay(this::checkAllTables, + initialDelay, + interval, + unit); + + logger.info("Auto-training manager started, checking every {} {} with initial delay {} {}.", + interval, unit, initialDelay, unit); + } + + @VisibleForTesting + void checkAllTables() + { + if (closed) + return; + + if (!isFirstCMSMember()) + return; + + for (ColumnFamilyStore cfs : getTables()) + { + if (!isEligibleTable(cfs)) + continue; + + try + { + checkTable(cfs); + } + catch (Exception e) + { + logger.warn("Error during auto-training check cycle for {}.{}", cfs.getKeyspaceName(), cfs.getTableName(), e); + } + } + } + + @VisibleForTesting + Iterable getTables() + { + return ColumnFamilyStore.all(); + } + + /** + * Returns {@code true} if this node is the first CMS member by lowest {@link NodeId}. + * Evaluated on every cycle so leadership migrates naturally as CMS membership changes. + */ + @VisibleForTesting + boolean isFirstCMSMember() + { + ClusterMetadata metadata = ClusterMetadata.current(); + List cmsMemberIds = new ArrayList<>(metadata.fullCMSMemberIds()); + cmsMemberIds.sort(Comparator.comparingInt(NodeId::id)); + + return !cmsMemberIds.isEmpty() && cmsMemberIds.get(0).equals(metadata.myNodeId()); + } + + public static boolean isEligibleTable(ColumnFamilyStore cfs) + { + if (SchemaConstants.isSystemKeyspace(cfs.getKeyspaceName())) + return false; + + if (cfs.isIndex()) + return false; + + if (cfs.metadata().isView()) + return false; + + return !cfs.metadata().isStaticCompactTable(); + } + + @VisibleForTesting + void checkTable(ColumnFamilyStore cfs) + { + if (!cfs.metadata().params.compression.isDictionaryCompressionEnabled()) + return; + + CompressionDictionaryManager manager = getCompressionDictionaryManager(cfs); + if (manager == null || !manager.isEnabled() || !manager.isAutoTrainingEnabled()) + return; + + if (manager.isTrainingRunning()) + { + logger.info("There is ongoing compression dictionary training for table {}.{}, " + + "skipping this round of auto-training.", + cfs.getKeyspaceName(), + cfs.getTableName()); + return; + } + + CompressionDictionary latest = retrieveLatestDictionary(cfs); + + if (latest == null) + { + logger.debug("There is no existing compression dictionary for table {}.{}, " + + "nothing to compare against, skipping this round of auto-training.", + cfs.getKeyspaceName(), + cfs.getTableName()); + return; + } + + if (!canConsume(cfs, latest)) + { + logger.debug("The latest compression dictionary for table {}.{} is of kind {}, which the current " + + "compressor cannot consume, so there is nothing comparable to evaluate against, " + + "skipping this round of auto-training.", + cfs.getKeyspaceName(), cfs.getTableName(), latest.kind()); + return; + } + + ColumnFamilyStore.RefViewFragment trainingFragment = resolveViewFragment(cfs); + + // proactively checking if there is a fragment to run auto-training over, + // if not then further logic does not make any sense + if (trainingFragment == null) + { + logger.debug("There is no SSTable view fragment to auto-train on for table {}.{}", + cfs.getKeyspaceName(), cfs.getTableName()); + return; + } + + // scheduleTraining hands trainingFragment to the training task, which releases it as its last + // act - and scheduleTraining blocks until that task has completed. The evaluation pass below + // reads the very same SSTables, so it must hold its own independent references; sharing one + // fragment would have it opening data channels on readers nothing references anymore, which a + // concurrent compaction is then free to tidy underneath it. + ColumnFamilyStore.RefViewFragment evaluationFragment = referenceAgain(trainingFragment); + + if (evaluationFragment == null) + { + logger.debug("Could not acquire a second set of SSTable references to evaluate on for table {}.{}, " + + "skipping this round of auto-training.", + cfs.getKeyspaceName(), cfs.getTableName()); + trainingFragment.close(); + return; + } + + try (evaluationFragment) + { + CompressionDictionary autoTrainedDictionary = scheduleTraining(cfs, trainingFragment, manager); + if (autoTrainedDictionary == null) + return; + + if (newDictionaryBetterThanLatest(cfs, + latest, + autoTrainedDictionary, + evaluationFragment, + manager.createTrainingConfig(Map.of()))) + { + manager.handleNewDictionary(autoTrainedDictionary); + } + } + } + + /** + * Takes a second, independent set of references to the SSTables of {@code fragment}, so that two + * consumers with different lifetimes can each own their references. + * + * @return a new fragment over the same SSTables, or {@code null} if any of them could no longer be referenced + */ + private static ColumnFamilyStore.RefViewFragment referenceAgain(ColumnFamilyStore.RefViewFragment fragment) + { + Refs refs = Refs.tryRef(fragment.sstables); + return refs == null ? null : new ColumnFamilyStore.RefViewFragment(fragment.sstables, fragment.memtables, refs); + } + + @VisibleForTesting + CompressionDictionaryManager getCompressionDictionaryManager(ColumnFamilyStore cfs) + { + return cfs.compressionDictionaryManager(); + } + + /** + * Returns {@code true} if the table's current compressor can consume {@code dictionary}, i.e. the dictionary + * is of a kind this compressor understands. + */ + @VisibleForTesting + static boolean canConsume(ColumnFamilyStore cfs, CompressionDictionary dictionary) + { + CompressionParams params = cfs.metadata().params.compression; + if (!params.isDictionaryCompressionEnabled()) + return false; + + return ((IDictionaryCompressor) params.getSstableCompressor()).canConsumeDictionary(dictionary); + } + + @VisibleForTesting + CompressionDictionary retrieveLatestDictionary(ColumnFamilyStore cfs) + { + return SystemDistributedKeyspace.retrieveLatestCompressionDictionary(cfs.getKeyspaceName(), + cfs.getTableName(), + cfs.metadata().id.toLongString()); + } + + @VisibleForTesting + ColumnFamilyStore.RefViewFragment resolveViewFragment(ColumnFamilyStore cfs) + { + CompressionDictionaryTrainingConfig config = getCompressionDictionaryManager(cfs).createTrainingConfig(Map.of()); + return new RecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment(); + } + + @VisibleForTesting + boolean newDictionaryBetterThanLatest(ColumnFamilyStore cfs, + CompressionDictionary latest, + CompressionDictionary autoTrainedDictionary, + ColumnFamilyStore.RefViewFragment refViewFragment, + CompressionDictionaryTrainingConfig config) + { + // The trainer must be started (moved into SAMPLING state) before the evaluator draws its held-out sample: + // createEvaluator() samples into the trainer in its constructor, and SSTableChunkSampler rejects samples + // unless the trainer is SAMPLING. Hence start(config) precedes createEvaluator(). + ICompressionDictionaryTrainer trainer = createTrainer(cfs); + trainer.start(config); + try (trainer; CompressionRatioEvaluator evaluator = createEvaluator(cfs, refViewFragment, trainer, config)) + { + double compressionRatioLatestDict = evaluator.evaluate(latest); + double compressionRatioAutoTrainedDict = evaluator.evaluate(autoTrainedDictionary); + + // A usable ratio is strictly positive. computeSampleCompressionRatio yields -1.0 + // (NO_COMPRESSION_RATIO) when nothing was sampled, and -1.0 is finite, so unguarded it + // inverts the comparison below: a -1.0 candidate - one that measured nothing at all - + // computes as +300% improvement and is promoted over a dictionary that works. + if (compressionRatioLatestDict <= 0 || compressionRatioAutoTrainedDict <= 0) + { + logger.debug("No usable compression ratio for table {}.{} (baseline={}, candidate={}); " + + "nothing was sampled, so there is nothing to compare and the candidate " + + "is not promoted.", + cfs.getKeyspaceName(), cfs.getTableName(), + compressionRatioLatestDict, compressionRatioAutoTrainedDict); + return false; + } + + double improvement = (compressionRatioLatestDict - compressionRatioAutoTrainedDict) / compressionRatioLatestDict; + + boolean isImprovement = improvement >= config.autoTrainingImprovementThreshold; + + String result = "Candidate will not be promoted."; + if (isImprovement) + result = "Candidate will be promoted."; + + logger.info("Auto-training candidate for {}.{}: baseline ratio={}, candidate ratio={}, improvement={}%, adoption threshold={}%. {}", + cfs.getKeyspaceName(), cfs.getTableName(), compressionRatioLatestDict, compressionRatioAutoTrainedDict, + String.format("%.3f", improvement * 100), + String.format("%.3f", config.autoTrainingImprovementThreshold * 100), + result); + + CompressionDictionaryAutoTrainingHistory.instance.record(cfs.getKeyspaceName(), + cfs.getTableName(), + autoTrainedDictionary.kind(), + compressionRatioLatestDict, + compressionRatioAutoTrainedDict, + improvement, + config.autoTrainingImprovementThreshold, + isImprovement); + + return isImprovement; + } + catch (Throwable t) + { + logger.warn("Unable to evaluate compression ratios of the latest and potential compression dictionary for {}.{}", + cfs.getKeyspaceName(), + cfs.getTableName(), + t); + } + + return false; + } + + @VisibleForTesting + ICompressionDictionaryTrainer createTrainer(ColumnFamilyStore cfs) + { + return ICompressionDictionaryTrainer.create(cfs.getKeyspaceName(), + cfs.getTableName(), + cfs.metadata().params.compression); + } + + @VisibleForTesting + CompressionRatioEvaluator createEvaluator(ColumnFamilyStore cfs, + ColumnFamilyStore.RefViewFragment refViewFragment, + ICompressionDictionaryTrainer trainer, + CompressionDictionaryTrainingConfig config) throws Throwable + { + return new CompressionRatioEvaluator(cfs.getKeyspaceName(), + cfs.getTableName(), + refViewFragment, + trainer, + config); + } + + private static class TrainedTableConsumer implements Consumer + { + public volatile CompressionDictionary trainedDictionary; + + @Override + public void accept(CompressionDictionary compressionDictionary) + { + this.trainedDictionary = compressionDictionary; + } + } + + /** + * Triggers dictionary training via {@link CompressionDictionaryManager#train(boolean, Map)}. + *

+ * The train method is synchronized and will throw if training is already in progress + * (e.g., triggered manually via nodetool) or if the min frequency hasn't elapsed + * (race between our check and an intervening manual training). Both cases are + * non-fatal and logged at debug level — the next check cycle will re-evaluate. + * + * @return dictionary trained dictionary, but not persisted yet + */ + @VisibleForTesting + CompressionDictionary scheduleTraining(ColumnFamilyStore cfs, + ColumnFamilyStore.RefViewFragment refViewFragment, + CompressionDictionaryManager manager) + { + try + { + TrainedTableConsumer trainedTableConsumer = new TrainedTableConsumer(); + // we need to wait until training has completed in order to progress + // TODO optionally we can timeout etc. + Future trainingFuture = manager.train(false, Collections.emptyMap(), refViewFragment, trainedTableConsumer); + if (trainingFuture != null) + { + trainingFuture.awaitUninterruptibly(); + return trainedTableConsumer.trainedDictionary; + } + else + throw new RuntimeException("Failed to start a trainer."); + } + catch (IllegalStateException e) + { + // Training already in progress — will retry on next cycle + logger.debug("Skipping auto-training for {}.{}: {}", cfs.getKeyspaceName(), cfs.getTableName(), e.getMessage()); + } + catch (IllegalArgumentException e) + { + // Min frequency not elapsed (race with manual training) — will retry on next cycle + logger.debug("Skipping auto-training for {}.{}: {}", cfs.getKeyspaceName(), cfs.getTableName(), e.getMessage()); + } + catch (Exception e) + { + logger.warn("Failed to trigger auto-training for {}.{}", cfs.getKeyspaceName(), cfs.getTableName(), e); + } + + return null; + } + + @Override + public synchronized void close() + { + if (closed) + return; + + logger.debug("Stopping auto-training manager."); + + closed = true; + if (checkTask != null) + { + checkTask.cancel(false); + checkTask = null; + } + if (executor != null) + { + try + { + ExecutorUtils.shutdownNowAndWait(1L, TimeUnit.MINUTES, executor); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + logger.warn("Interrupted while stopping the auto-training executor."); + } + catch (TimeoutException e) + { + logger.warn("Auto-training executor did not stop within a minute."); + } + executor = null; + } + logger.debug("Auto-training manager stopped."); + } + + public static class CompressionRatioEvaluator implements AutoCloseable + { + private final String keyspaceName; + private final String tableName; + private final ColumnFamilyStore.RefViewFragment refViewFragment; + private final ICompressionDictionaryTrainer trainer; + private final CompressionDictionaryTrainingConfig config; + + private CompressionRatioEvaluator(String keyspaceName, + String tableName, + ColumnFamilyStore.RefViewFragment refViewFragment, + ICompressionDictionaryTrainer trainer, + CompressionDictionaryTrainingConfig config) throws Throwable + { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.refViewFragment = refViewFragment; + this.trainer = trainer; + this.config = config; + + performSampling(); + } + + public void performSampling() throws Throwable + { + try + { + logger.info("Sampling chunks from {} SSTables for {}.{}", refViewFragment.sstables.size(), keyspaceName, tableName); + + // Sample chunks from SSTables and add to trainer + SSTableChunkSampler.sampleFromSSTables(refViewFragment.sstables, trainer, config); + + logger.info("Completed sampling for {}.{}, now computing compression ratio", keyspaceName, tableName); + } + catch (Exception e) + { + logger.error("Failed to sample from SSTables for {}.{}", keyspaceName, tableName, e); + throw e; + } + } + + public double evaluate(CompressionDictionary dictionary) + { + double ratio = trainer.computeSampleCompressionRatio(dictionary.rawDictionary()); + // round to at most 5 decimal places + return Double.isFinite(ratio) ? Math.round(ratio * 100_000.0) / 100_000.0 : ratio; + } + + @Override + public void close() throws Exception + { + try + { + trainer.close(); + } + catch (Throwable t) + { + logger.debug("Unable to close sample compression ratio trainer.", t); + } + // refViewFragment is owned by the caller, which closes it; the evaluator only borrows it. + } + } +} diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryCache.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryCache.java index 92b97ef5ef5c..0bc16b4d6ddd 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryCache.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryCache.java @@ -102,14 +102,19 @@ public CompressionDictionary get(DictId dictId) } @Override - public void add(@Nullable CompressionDictionary compressionDictionary) + public CompressionDictionary add(@Nullable CompressionDictionary compressionDictionary) { if (compressionDictionary == null) - return; - - // Only update cache if not already in the cache + return null; + + // Only update cache if not already in the cache. cache.get(key, mappingFunction) returns the + // CANONICAL cached instance: either the argument (on a miss, after initRefLazily makes the cache + // the owner of its selfRef and it is stored) or the instance that already won the cache for this + // dictId (on a hit/race, in which case our argument is a redundant loser whose selfRef was never + // created). We return that canonical instance so callers reference the managed dictionary rather + // than a loser whose selfRef the cache would never own or release (CASSANDRA-21047 leak). DictId newDictId = compressionDictionary.dictId(); - cache.get(newDictId, id -> { + CompressionDictionary cached = cache.get(newDictId, id -> { Ref ref = compressionDictionary.initRefLazily(); if (ref == null) { @@ -125,6 +130,8 @@ public void add(@Nullable CompressionDictionary compressionDictionary) { currentId = currentDictId.get(); } + + return cached; } @Override diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryDetailsTabularData.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryDetailsTabularData.java index 8b55abbb482c..b36bda357952 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryDetailsTabularData.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryDetailsTabularData.java @@ -170,7 +170,7 @@ public static CompositeData fromCompressionDictionary(String keyspace, String ta dictionary.kind().name(), dictionary.checksum(), dictionary.rawDictionary().length, - dictionary.createdAt().toString(), + dictionary.createdAt().toString() }); } catch (OpenDataException e) diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryEventHandler.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryEventHandler.java index ed4d270d0c8f..d8a1b43ab6fc 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryEventHandler.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryEventHandler.java @@ -88,6 +88,16 @@ public void onNewDictionaryAvailable(CompressionDictionary.DictId dictionaryId) } CompressionDictionary dictionary = SystemDistributedKeyspace.retrieveCompressionDictionary(keyspaceName, tableName, cfs.metadata().id.toLongString(), dictionaryId.id); + if (dictionary == null) + { + // cache.add(null) is a no-op, so without this the node keeps writing with the dictionary it + // already has and nothing anywhere says so. The periodic refresh is the only recovery, and + // it runs on compression_dictionary_refresh_interval - an hour by default. + logger.warn("Notified of compression dictionary {} for {}.{} but could not read it back; " + + "this node keeps using its current dictionary until the next periodic refresh.", + dictionaryId, keyspaceName, tableName); + return; + } cache.add(dictionary); } catch (Exception e) diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryManager.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryManager.java index 264e318f439c..fcc78f6577a7 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryManager.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryManager.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import javax.annotation.Nullable; import javax.management.openmbean.CompositeData; @@ -43,6 +44,7 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.MBeanWrapper.OnException; +import org.apache.cassandra.utils.concurrent.Future; import static java.lang.String.format; import static org.apache.cassandra.schema.SystemDistributedKeyspace.retrieveLightweightLatestCompressionDictionary; @@ -102,6 +104,11 @@ public boolean isEnabled() return isEnabled; } + public boolean isTrainingRunning() + { + return scheduler.isTrainingRunning(); + } + /** * Reloads dictionary management configuration when compression parameters change. * This method enables or disables dictionary compression based on the new parameters, @@ -148,9 +155,9 @@ public CompressionDictionary get(CompressionDictionary.DictId dictId) } @Override - public void add(@Nullable CompressionDictionary compressionDictionary) + public CompressionDictionary add(@Nullable CompressionDictionary compressionDictionary) { - cache.add(compressionDictionary); + return cache.add(compressionDictionary); } @Override @@ -174,22 +181,21 @@ public void onNewDictionaryAvailable(CompressionDictionary.DictId dictionaryId) @Override public synchronized void train(boolean force, Map parameters) { - // Validate table supports dictionary compression + // A non-dictionary table cannot be trained; reject it before anything else, matching the contract of + // train(force, parameters, refViewFragment, listener). if (!isEnabled) - { throw new UnsupportedOperationException("Table " + keyspaceName + '.' + tableName + " does not support dictionary compression"); - } - // resolve training config and fail fast when invalid, so we do not reach logic which would e.g. flush unnecessarily. + // Fail fast on invalid training parameters before selecting a view or flushing: an unnecessary flush (and a + // misleading "No SSTables" error on an empty table) is worse than rejecting the bad parameters up front. CompressionDictionaryTrainingConfig trainingConfig = createTrainingConfig(parameters); - LightweightCompressionDictionary dictionary = retrieveLightweightLatestCompressionDictionary(columnFamilyStore.getKeyspaceName(), - columnFamilyStore.getTableName(), - columnFamilyStore.metadata.id.toLongString()); - - checkTrainingFrequency(dictionary, trainingConfig); - - // SSTable-based training: sample from existing SSTables + // Likewise reject a too-soon retrain up front. train(4-arg) checks this again for the benefit of its other + // callers, but by then we would already have flushed and referenced SSTables for a request we then reject. + checkTrainingFrequency(retrieveLightweightLatestCompressionDictionary(columnFamilyStore.getKeyspaceName(), + columnFamilyStore.getTableName(), + columnFamilyStore.metadata.id.toLongString()), + trainingConfig); // this is not closed here but in training runnable when finished // also, if view is empty, and we throw just below because of it then @@ -209,11 +215,50 @@ public synchronized void train(boolean force, Map parameters) } } - scheduler.scheduleSSTableBasedTraining(refViewFragment, - compressionParams, - trainingConfig, - this::handleNewDictionary, - force); + train(force, parameters, refViewFragment, this::handleNewDictionary); + } + + public synchronized Future train(boolean force, Map parameters, + ColumnFamilyStore.RefViewFragment refViewFragment, + Consumer listener) + { + // This method takes ownership of refViewFragment: on success the scheduler releases it when the + // training task finishes. Every path that returns or throws before reaching the scheduler must + // therefore release it here, otherwise the SSTable references stay pinned for the life of the node + // and the files can never be deleted after compaction. + try + { + // Validate table supports dictionary compression + if (!isEnabled) + { + throw new UnsupportedOperationException("Table " + keyspaceName + '.' + tableName + " does not support dictionary compression"); + } + + if (refViewFragment.sstables.isEmpty()) + throw new IllegalArgumentException("No SSTables to train on for table " + keyspaceName + '.' + tableName); + + // resolve training config and fail fast when invalid, so we do not reach logic which would e.g. flush unnecessarily. + CompressionDictionaryTrainingConfig trainingConfig = createTrainingConfig(parameters); + + LightweightCompressionDictionary dictionary = retrieveLightweightLatestCompressionDictionary(columnFamilyStore.getKeyspaceName(), + columnFamilyStore.getTableName(), + columnFamilyStore.metadata.id.toLongString()); + + checkTrainingFrequency(dictionary, trainingConfig); + + // SSTable-based training: sample from existing SSTables + + return scheduler.scheduleSSTableBasedTraining(refViewFragment, + compressionParams, + trainingConfig, + listener, + force); + } + catch (Throwable t) + { + refViewFragment.close(); + throw t; + } } @Override @@ -300,7 +345,14 @@ public synchronized void importCompressionDictionary(CompositeData compositeData checkTrainingFrequency(latestCompressionDictionary, createTrainingConfig(Map.of())); } - handleNewDictionary(kind.createDictionary(dictId, dataObject.dict, dataObject.dictChecksum)); + handleNewDictionary(kind.createDictionary(dictId, dataObject.dict, dataObject.dictChecksum, dataObject.createdAt)); + } + + @Override + public boolean isAutoTrainingEnabled() + { + // derived from the live params so ALTER TABLE takes effect without a restart + return CompressionDictionaryTrainingConfig.isAutoTrainingEnabled(compressionParams.getOtherOptions()); } /** @@ -314,9 +366,9 @@ public synchronized void close() closeQuitely(scheduler, "CompressionDictionaryScheduler"); } - void handleNewDictionary(CompressionDictionary dictionary) + public void handleNewDictionary(CompressionDictionary dictionary) { - // sequence meatters; persist the new dictionary before broadcasting to others. + // sequence matters; persist the new dictionary before broadcasting to others. storeDictionary(dictionary); onNewDictionaryTrained(dictionary.dictId()); } @@ -329,7 +381,7 @@ void handleNewDictionary(CompressionDictionary dictionary) * for a given table will be used * @return training configuration with max dictionary size and total sample size of supplied arguments. */ - private CompressionDictionaryTrainingConfig createTrainingConfig(Map parameters) + public CompressionDictionaryTrainingConfig createTrainingConfig(Map parameters) { return CompressionDictionaryTrainingConfig .builder() @@ -337,10 +389,12 @@ private CompressionDictionaryTrainingConfig createTrainingConfig(Map scheduledRefreshTask; private volatile boolean isEnabled; + /** + * Dictionary training samples from disk and runs training, which might take minutes for a large table. + * It gets its own thread rather than one of the shared single-threaded pools: NonPeriodicTasks in particular + * also runs SSTable tidying and LogTransaction deletions, so blocking it stalls the removal of obsoleted + * SSTables. Node-wide and deliberately never shut down by close(), which runs per table on drop and when an + * ALTER disables dictionary compression; shutting a shared executor down there would stop training for every + * other table. Its threads are daemon, so an idle pool cannot hold the JVM open. It is terminated only + * at node shutdown, through {@link #shutdownNowAndWait(long, TimeUnit)}. + */ + private static final ScheduledExecutorPlus TRAINING_EXECUTOR = + ExecutorFactory.Global.executorFactory().scheduled(false, "CompressionDictionaryTraining"); + public CompressionDictionaryScheduler(String keyspaceName, String tableName, String tableId, @@ -88,11 +103,11 @@ public void scheduleRefreshTask() } @Override - public void scheduleSSTableBasedTraining(ColumnFamilyStore.RefViewFragment refViewFragment, - CompressionParams compressionParams, - CompressionDictionaryTrainingConfig config, - Consumer listener, - boolean force) + public Future scheduleSSTableBasedTraining(ColumnFamilyStore.RefViewFragment refViewFragment, + CompressionParams compressionParams, + CompressionDictionaryTrainingConfig config, + Consumer listener, + boolean force) { if (!trainingInProgress.compareAndSet(false, true)) { @@ -124,13 +139,24 @@ public void scheduleSSTableBasedTraining(ColumnFamilyStore.RefViewFragment refVi SSTableSamplingTask task = new SSTableSamplingTask(refViewFragment, trainer, config, force); // trainer is eventually closed here, as well as indicating // in manualTrainingInProgress that it was finished - ScheduledExecutors.nonPeriodicTasks.submit(task); + try + { + return TRAINING_EXECUTOR.submit(task); + } + catch (Throwable t) + { + // the task will never run, so nothing else will release these + finishTraining(trainer.getTrainingState()); + cleanup(refViewFragment, trainer); + throw t; + } } else { finishTraining(trainer.getTrainingState()); cleanup(refViewFragment, trainer); } + return null; } /** @@ -186,6 +212,12 @@ private void refreshDictionaryFromSystemTable() } } + @Override + public boolean isTrainingRunning() + { + return trainingInProgress.get(); + } + @Override public void close() { @@ -198,6 +230,15 @@ public void close() finishTraining(TrainingState.notStarted()); } + /** + * Terminates the node-wide training executor. For node shutdown only: per-table {@link #close()} must not call + * this, or disabling dictionary compression on one table would stop training for every other table. + */ + public static void shutdownNowAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException + { + ExecutorUtils.shutdownNowAndWait(timeout, unit, TRAINING_EXECUTOR); + } + /** * Task that samples chunks from existing SSTables and triggers training. * Acquires references to SSTables to prevent them from being deleted during sampling. @@ -234,27 +275,34 @@ public void run() logger.info("Completed sampling for {}.{}, now training dictionary", keyspaceName, tableName); + Throwable trainingThrowable = null; // Use the force parameter from the task - trainer.trainDictionaryAsync(force) - .addCallback((dictionary, throwable) -> { - if (throwable != null) - { - logger.error("SSTable-based dictionary training failed for {}.{}: {}", - keyspaceName, tableName, throwable.getMessage()); - } - else - { - logger.info("SSTable-based dictionary training completed for {}.{}", - keyspaceName, tableName); - } - - finishTraining(trainer.getTrainingState()); - cleanup(refViewFragment, trainer); - }); + try + { + trainer.trainDictionary(force); + } + catch (Throwable t) + { + trainingThrowable = t; + } + + if (trainingThrowable != null) + { + logger.error("SSTable-based dictionary training failed for {}.{}: {}", + keyspaceName, tableName, trainingThrowable.getMessage()); + } + else + { + logger.info("SSTable-based dictionary training completed for {}.{}", + keyspaceName, tableName); + } } catch (Exception e) { logger.error("Failed to sample from SSTables for {}.{}", keyspaceName, tableName, e); + } + finally + { finishTraining(trainer.getTrainingState()); cleanup(refViewFragment, trainer); } @@ -273,10 +321,4 @@ private void cleanup(ColumnFamilyStore.RefViewFragment refViewFragment, ICompres } refViewFragment.close(); } - - @VisibleForTesting - boolean isTrainingRunning() - { - return trainingInProgress.get(); - } } diff --git a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryTrainingConfig.java b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryTrainingConfig.java index 3e73d8c0e013..71e4cf8e5d91 100644 --- a/src/java/org/apache/cassandra/db/compression/CompressionDictionaryTrainingConfig.java +++ b/src/java/org/apache/cassandra/db/compression/CompressionDictionaryTrainingConfig.java @@ -28,6 +28,11 @@ import org.apache.cassandra.schema.CompressionParams; import static java.lang.String.format; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.AUTO_TRAINING_ENABLED; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.AUTO_TRAINING_IMPROVEMENT_THRESHOLD_NAME; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.AUTO_TRAINING_TWCS_MAX_WINDOWS; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE; +import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_AUTO_TRAINING_TWCS_MAX_WINDOWS_VALUE; import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_TRAINING_MAX_DICTIONARY_SIZE_PARAMETER_VALUE; import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_TRAINING_MAX_TOTAL_SAMPLE_SIZE_PARAMETER_VALUE; import static org.apache.cassandra.io.compress.IDictionaryCompressor.DEFAULT_TRAINING_MIN_FREQUENCY; @@ -45,6 +50,9 @@ public class CompressionDictionaryTrainingConfig public final int acceptableTotalSampleSize; public final int chunkSize; public final int minTrainingFrequency; + public final boolean autoTrainingEnabled; + public final float autoTrainingImprovementThreshold; + public final int autoTrainingTwcsMaxWindows; private CompressionDictionaryTrainingConfig(Builder builder) { @@ -53,6 +61,9 @@ private CompressionDictionaryTrainingConfig(Builder builder) this.acceptableTotalSampleSize = builder.maxTotalSampleSize / 10 * 8; this.chunkSize = builder.chunkSize; this.minTrainingFrequency = builder.minTrainingFrequency; + this.autoTrainingEnabled = builder.autoTrainingEnabled; + this.autoTrainingImprovementThreshold = builder.autoTrainingImprovementThreshold; + this.autoTrainingTwcsMaxWindows = builder.autoTrainingTwcsMaxWindows; } public static Builder builder() @@ -66,6 +77,9 @@ public static class Builder private int maxTotalSampleSize = 10 * 1024 * 1024; // 10MB total private int chunkSize = 64 * 1024; // 64KB default private int minTrainingFrequency = 0; // in minutes + private boolean autoTrainingEnabled = false; + private float autoTrainingImprovementThreshold = Float.parseFloat(DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE); + private int autoTrainingTwcsMaxWindows = Integer.parseInt(DEFAULT_AUTO_TRAINING_TWCS_MAX_WINDOWS_VALUE); public Builder maxDictionarySize(int size) { @@ -91,12 +105,33 @@ public Builder minTrainingFrequency(int minTrainingFrequency) return this; } + public Builder isAutoTrainingEnabled(boolean autoTrainingEnabled) + { + this.autoTrainingEnabled = autoTrainingEnabled; + return this; + } + + public Builder autoTrainingImprovementThreshold(float improvement) + { + this.autoTrainingImprovementThreshold = improvement; + return this; + } + + public Builder autoTrainingTwcsMaxWindows(int autoTrainingTwcsMaxWindows) + { + this.autoTrainingTwcsMaxWindows = autoTrainingTwcsMaxWindows; + return this; + } + public CompressionDictionaryTrainingConfig build() { Preconditions.checkArgument(maxDictionarySize > 0, "maxDictionarySize must be positive"); Preconditions.checkArgument(maxTotalSampleSize > 0, "maxTotalSampleSize must be positive"); Preconditions.checkArgument(chunkSize > 0, "chunkSize must be positive"); Preconditions.checkArgument(minTrainingFrequency >= 0, "min training frequency must be non-negative"); + Preconditions.checkArgument(autoTrainingImprovementThreshold > 0 && autoTrainingImprovementThreshold < 1, + "auto training improvement threshold has to be (0, 1)"); + Preconditions.checkArgument(autoTrainingTwcsMaxWindows > 0, "auto_training_twcs_max_windows must be positive"); return new CompressionDictionaryTrainingConfig(this); } } @@ -138,6 +173,58 @@ public static int getMaxTotalSampleSizeWithUserSuppliedParams(CompressionParams DEFAULT_TRAINING_MAX_TOTAL_SAMPLE_SIZE_PARAMETER_VALUE); } + public static boolean isAutoTrainingEnabled(Map params) + { + return validateBoolean(AUTO_TRAINING_ENABLED, + params.getOrDefault(AUTO_TRAINING_ENABLED, "false")); + } + + public static float getAutoTrainingImprovementThreshold(Map params) + { + float parsedValue; + + try + { + String resolvedValue = params.getOrDefault(AUTO_TRAINING_IMPROVEMENT_THRESHOLD_NAME, + DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE); + + parsedValue = Float.parseFloat(resolvedValue); + } + catch (Throwable t) + { + throw new ConfigurationException(String.format("Unable to parse float value for %s", + DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE)); + } + + if (Float.isInfinite(parsedValue) || Float.isNaN(parsedValue) || parsedValue <= 0f || parsedValue >= 1f) + throw new ConfigurationException(String.format("Invalid value for %s, can be (0, 1]", + AUTO_TRAINING_IMPROVEMENT_THRESHOLD_NAME)); + + return parsedValue; + } + + public static int getAutoTrainingTwcsMaxWindows(Map params) + { + String resolvedValue = params.getOrDefault(AUTO_TRAINING_TWCS_MAX_WINDOWS, + DEFAULT_AUTO_TRAINING_TWCS_MAX_WINDOWS_VALUE); + int parsedValue; + try + { + parsedValue = Integer.parseInt(resolvedValue.trim()); + } + catch (Throwable t) + { + throw new ConfigurationException(format("Unable to set value to parameter %s: %s. It has to be a positive integer.", + AUTO_TRAINING_TWCS_MAX_WINDOWS, resolvedValue)); + } + + if (parsedValue < 1) + throw new ConfigurationException(format("Invalid value for %s: %s. It has to be a positive integer (>= 1).", + AUTO_TRAINING_TWCS_MAX_WINDOWS, resolvedValue)); + + return parsedValue; + } + private static int internalTrainingParameterResolution(CompressionParams compressionParams, String userSuppliedValue, String parameterName, @@ -202,4 +289,20 @@ static int validateDurationBasedTrainingParameter(String parameterName, String r parameterName, resolvedValue, t.getMessage())); } } + + static boolean validateBoolean(String parameterName, String resolvedValue) + { + if (resolvedValue == null) + return false; + + if (resolvedValue.equals("true") || resolvedValue.equals("false")) + { + return Boolean.parseBoolean(resolvedValue); + } + else + { + throw new ConfigurationException(format("Unable to set value to parameter %s: %s. It has to be 'true' or 'false'.", + parameterName, resolvedValue)); + } + } } diff --git a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryCache.java b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryCache.java index 638fefc823cd..a0aafd3ed948 100644 --- a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryCache.java +++ b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryCache.java @@ -49,10 +49,17 @@ public interface ICompressionDictionaryCache extends AutoCloseable /** * Stores a compression dictionary in the local cache and updates the current dictionary if the new one is newer. + *

+ * Returns the CANONICAL cached instance for this dictionary's id: either {@code compressionDictionary} + * itself (when it populates the cache) or the instance already cached for the same id (when a concurrent + * add won the race). Callers that go on to reference the dictionary MUST use the returned instance rather + * than their argument — referencing a redundant "loser" instance would lazily create a selfRef the cache + * never owns and therefore never releases, leaking it (CASSANDRA-21047). * * @param compressionDictionary the compression dictionary to cache, may be null + * @return the canonical cached dictionary for this id, or null if the argument was null */ - void add(@Nullable CompressionDictionary compressionDictionary); + CompressionDictionary add(@Nullable CompressionDictionary compressionDictionary); /** * Gives number of bytes cached compression dictionaries occupy in this cache. diff --git a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryScheduler.java b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryScheduler.java index 5786414f4fec..bee7071332e0 100644 --- a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryScheduler.java +++ b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryScheduler.java @@ -22,6 +22,7 @@ import org.apache.cassandra.db.ColumnFamilyStore.RefViewFragment; import org.apache.cassandra.schema.CompressionParams; +import org.apache.cassandra.utils.concurrent.Future; /** * Interface for managing scheduled tasks for compression dictionary operations. @@ -40,26 +41,27 @@ public interface ICompressionDictionaryScheduler extends AutoCloseable /** * Schedules SSTable-based training that samples from existing SSTables. - * + *

* A caller of this method should ensure that SSTables referred in {@code refViewFragment} are closed * eventually, either directly at the end of that method or by other means, when training is running * asynchronously. - * + *

* A caller of this method might assume that {@code trainer} might be closed after this method finishes, either * directly in this method or indirectly when training is running asynchronously. * - * @param refViewFragment the view of SSTables to sample from + * @param refViewFragment the view of SSTables to sample from * @param compressionParams parameters for compression - * @param config the training configuration - * @param listener listener invoked when a dictionary is trained - * @param force force the dictionary training even if there are not enough samples + * @param config the training configuration + * @param listener listener invoked when a dictionary is trained + * @param force force the dictionary training even if there are not enough samples + * @return training future which runs the actual training or null if no training was scheduled * @throws IllegalStateException if training is already in progress */ - void scheduleSSTableBasedTraining(RefViewFragment refViewFragment, - CompressionParams compressionParams, - CompressionDictionaryTrainingConfig config, - Consumer listener, - boolean force); + Future scheduleSSTableBasedTraining(RefViewFragment refViewFragment, + CompressionParams compressionParams, + CompressionDictionaryTrainingConfig config, + Consumer listener, + boolean force); /** * Sets the enabled state of the scheduler. When disabled, refresh tasks will not execute. @@ -69,4 +71,11 @@ void scheduleSSTableBasedTraining(RefViewFragment refViewFragment, void setEnabled(boolean enabled); TrainingState getLastTrainingState(); + + /** + * Returns whether a training is running or not. + * + * @return true if there is a training running against a table, false otherwise + */ + boolean isTrainingRunning(); } diff --git a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryTrainer.java b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryTrainer.java index 708baa2a8a5f..14a314c28b50 100644 --- a/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryTrainer.java +++ b/src/java/org/apache/cassandra/db/compression/ICompressionDictionaryTrainer.java @@ -63,6 +63,11 @@ public interface ICompressionDictionaryTrainer extends AutoCloseable */ CompressionDictionary trainDictionary(boolean force); + /** + * Get compression ratio of a given dictionary bytes against sampled data this trainer holds + */ + double computeSampleCompressionRatio(byte[] dictBytes); + /** * Trains and produces a compression dictionary from collected samples asynchronously. * diff --git a/src/java/org/apache/cassandra/db/compression/RecencyBiasViewFragmentResolver.java b/src/java/org/apache/cassandra/db/compression/RecencyBiasViewFragmentResolver.java new file mode 100644 index 000000000000..ab0503c6e0a9 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compression/RecencyBiasViewFragmentResolver.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.HashSet; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy; +import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategyOptions; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.lifecycle.View; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; + +/** + * Resolves the SSTable {@link ColumnFamilyStore.RefViewFragment} that auto-training should sample from, biased + * toward recent data so training can detect that the data has drifted (training over the whole dataset would just + * relearn the mostly-old distribution and never notice the change). + *

+ * Only {@link TimeWindowCompactionStrategy} is supported for now. A TWCS time window is the unit of selection: the + * resolver walks windows newest-first and adds each window whole (all of its SSTables), accumulating their + * uncompressed size, and stops as soon as it has gathered {@link CompressionDictionaryTrainingConfig#acceptableTotalSampleSize} + * bytes - the same floor the trainer requires to consider a sample big enough to train on. It never reaches back more + * than {@code auto_training_twcs_max_windows} windows (default 1, i.e. the newest window only), so recency is bounded + * even when the data is thin. + *

+ * Resolution yields {@code null} - which the caller treats as "nothing to train on this round" and backs off - when + * the table is not on TWCS, when it has no SSTables, or when the recent windows within the cap do not hold enough + * data to reach the sample floor (training on a too-small sample is worse than waiting for more data to arrive). + */ +public class RecencyBiasViewFragmentResolver +{ + private static final Logger logger = LoggerFactory.getLogger(RecencyBiasViewFragmentResolver.class); + + protected final ColumnFamilyStore cfs; + protected final CompressionDictionaryTrainingConfig config; + + public RecencyBiasViewFragmentResolver(ColumnFamilyStore cfs, CompressionDictionaryTrainingConfig config) + { + this.cfs = cfs; + this.config = config; + } + + public ColumnFamilyStore.RefViewFragment resolveViewFragment() + { + Class compactionStrategyClass = cfs.getCompactionStrategyManager().getCompactionParams().klass(); + + if (TimeWindowCompactionStrategy.class.isAssignableFrom(compactionStrategyClass)) + return new TimeWindowRecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment(); + + return null; + } + + /** + * Selects whole TWCS time windows, newest-first, until enough uncompressed data has been gathered to train on + * (or the configured window cap is hit). + */ + public static class TimeWindowRecencyBiasViewFragmentResolver extends RecencyBiasViewFragmentResolver + { + public TimeWindowRecencyBiasViewFragmentResolver(ColumnFamilyStore cfs, CompressionDictionaryTrainingConfig config) + { + super(cfs, config); + } + + @Override + public ColumnFamilyStore.RefViewFragment resolveViewFragment() + { + Map options = cfs.getCompactionStrategyManager().getCompactionParams().options(); + + TimeUnit windowUnit = options.containsKey(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_UNIT_KEY) + ? TimeUnit.valueOf(options.get(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_UNIT_KEY)) + : TimeUnit.DAYS; + int windowSize = options.containsKey(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_SIZE_KEY) + ? Integer.parseInt(options.get(TimeWindowCompactionStrategyOptions.COMPACTION_WINDOW_SIZE_KEY)) + : 1; + TimeUnit timestampResolution = options.containsKey(TimeWindowCompactionStrategyOptions.TIMESTAMP_RESOLUTION_KEY) + ? TimeUnit.valueOf(options.get(TimeWindowCompactionStrategyOptions.TIMESTAMP_RESOLUTION_KEY)) + : TimeUnit.MICROSECONDS; + + // Bucket SSTables into TWCS windows and total the uncompressed bytes each window holds. Only windows that + // actually contain SSTables become keys, so empty windows - gaps in the timeline, including an as-yet- + // unflushed current window - are simply absent here. The map is ordered by window lower bound, so its + // descending key set is the non-empty windows, newest-first. + NavigableMap uncompressedBytesByWindow = new TreeMap<>(); + for (SSTableReader sstable : cfs.getSSTables(SSTableSet.CANONICAL)) + { + long window = windowLowerBound(sstable, windowUnit, windowSize, timestampResolution); + uncompressedBytesByWindow.merge(window, sstable.uncompressedLength(), Long::sum); + } + + if (uncompressedBytesByWindow.isEmpty()) + return null; + + // Add whole windows, newest-first, until we have enough sample data to train on or we have reached back + // as far as the configured cap allows. A window is never split: it contributes all of its SSTables. Since + // we only ever iterate non-empty windows, empty windows are skipped for free and never spend the cap - the + // cap bounds how many windows holding data we consume, not how far back in time we look. + Set selectedWindows = new HashSet<>(); + long accumulatedBytes = 0; + for (Long window : uncompressedBytesByWindow.descendingKeySet()) + { + if (selectedWindows.size() >= config.autoTrainingTwcsMaxWindows) + break; + + selectedWindows.add(window); + accumulatedBytes += uncompressedBytesByWindow.get(window); + + if (accumulatedBytes >= config.acceptableTotalSampleSize) + break; + } + + // Even after reaching back as far as the cap allows, the recent windows may not hold enough data to train + // on. Rather than train on a too-small sample, back off: return null so the auto-trainer skips this round. + if (accumulatedBytes < config.acceptableTotalSampleSize) + { + logger.debug("Recency-biased sampling for {}.{} gathered only {} across the {} most-recent TWCS window(s) " + + "(auto_training_twcs_max_windows={}), below the {} needed to train; backing off this round.", + cfs.getKeyspaceName(), cfs.getTableName(), + FileUtils.stringifyFileSize(accumulatedBytes, true), + selectedWindows.size(), config.autoTrainingTwcsMaxWindows, + FileUtils.stringifyFileSize(config.acceptableTotalSampleSize, true)); + return null; + } + + ColumnFamilyStore.RefViewFragment fragment = + cfs.selectAndReference(View.select(SSTableSet.CANONICAL, + sstable -> selectedWindows.contains(windowLowerBound(sstable, windowUnit, windowSize, timestampResolution)))); + + // The window accounting above ran against an earlier, unreferenced view. A compaction completing since + // then can leave nothing matching the predicate, so return null as documented rather than an empty + // fragment the caller would mistake for work to do. + if (fragment.sstables.isEmpty()) + { + logger.debug("Recency-biased sampling for {}.{} selected no SSTables; they were compacted away while resolving.", + cfs.getKeyspaceName(), cfs.getTableName()); + fragment.close(); + return null; + } + + return fragment; + } + + /** + * The lower bound (in ms) of the TWCS time window an SSTable falls into, computed the same way TWCS + * buckets SSTables: its max timestamp, converted from the table's timestamp resolution to ms, snapped + * down to the window. + */ + private static long windowLowerBound(SSTableReader sstable, TimeUnit windowUnit, int windowSize, TimeUnit timestampResolution) + { + long timestampInMillis = TimeUnit.MILLISECONDS.convert(sstable.getMaxTimestamp(), timestampResolution); + return TimeWindowCompactionStrategy.getWindowBoundsInMillis(windowUnit, windowSize, timestampInMillis).left; + } + } +} diff --git a/src/java/org/apache/cassandra/db/compression/ZstdCompressionDictionary.java b/src/java/org/apache/cassandra/db/compression/ZstdCompressionDictionary.java index a83d1f49106e..bd0b150e2363 100644 --- a/src/java/org/apache/cassandra/db/compression/ZstdCompressionDictionary.java +++ b/src/java/org/apache/cassandra/db/compression/ZstdCompressionDictionary.java @@ -20,9 +20,13 @@ import java.time.Instant; import java.util.Objects; +import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; +import com.github.luben.zstd.ZstdCompressCtx; +import com.github.luben.zstd.ZstdDecompressCtx; import com.github.luben.zstd.ZstdDictCompress; import com.github.luben.zstd.ZstdDictDecompress; import com.google.common.annotations.VisibleForTesting; @@ -46,6 +50,12 @@ public class ZstdCompressionDictionary implements CompressionDictionary, SelfRef // One ZstdDictDecompress and multiple ZstdDictCompress (per level) can be derived from the same raw dictionary content private final ConcurrentHashMap zstdDictCompressPerLevel = new ConcurrentHashMap<>(); private final AtomicReference dictDecompress = new AtomicReference<>(); + // Reusable native (de)compression contexts, pooled and borrowed per chunk by compressors sharing this + // dictionary. Compress contexts are keyed by level (the loaded CDict is level-specific); decompress contexts + // share one pool since a single DDict serves all levels. Closed by Tidy alongside the dictionary tables they + // were loaded with, so no context can outlive the native memory it references. + private final ConcurrentHashMap> compressCtxPoolPerLevel = new ConcurrentHashMap<>(); + private final Queue decompressCtxPool = new ConcurrentLinkedQueue<>(); private volatile Ref selfRef; private final Instant createdAt; @@ -189,6 +199,100 @@ public ZstdDictDecompress dictionaryForDecompression() } } + /** + * Borrow a pooled compression context with the dictionary for {@code compressionLevel} already loaded, + * creating one if the pool is empty. Reusing a context avoids allocating a native ZSTD_CCtx per call. + *
+ * IMPORTANT: Caller MUST hold a valid reference (via tryRef/ref) to this dictionary for as long as the + * borrowed context is in use, and must return it via {@link #releaseCompressCtx(int, ZstdCompressCtx)}. + * + * @param compressionLevel compression level the context should be loaded for + * @return a borrowed context; caller owns it exclusively until released + * @throws IllegalStateException if called without holding a valid reference + */ + public ZstdCompressCtx acquireCompressCtx(int compressionLevel) + { + ensureNotReleased(); + Queue pool = compressCtxPoolPerLevel.computeIfAbsent(compressionLevel, level -> new ConcurrentLinkedQueue<>()); + ZstdCompressCtx ctx = pool.poll(); + if (ctx == null) + // The compression level is carried by the precomputed CDict, so loadDict is all that is required. + ctx = new ZstdCompressCtx().loadDict(dictionaryForCompression(compressionLevel)); + return ctx; + } + + /** + * Return a context borrowed from {@link #acquireCompressCtx(int)}. If this dictionary has since been + * released, the context is closed instead of pooled, so it does not outlive the dictionary's native memory. + */ + public void releaseCompressCtx(int compressionLevel, ZstdCompressCtx ctx) + { + Queue pool = compressCtxPoolPerLevel.computeIfAbsent(compressionLevel, level -> new ConcurrentLinkedQueue<>()); + pool.offer(ctx); + if (selfRef == null || selfRef.globalCount() <= 0) // released concurrently — ensure it is not leaked + drainCompressPool(pool); + } + + /** + * Borrow a pooled decompression context with the dictionary already loaded, creating one if the pool is empty. + *
+ * IMPORTANT: Caller MUST hold a valid reference (via tryRef/ref) to this dictionary for as long as the + * borrowed context is in use, and must return it via {@link #releaseDecompressCtx(ZstdDecompressCtx)}. + * + * @throws IllegalStateException if called without holding a valid reference + */ + public ZstdDecompressCtx acquireDecompressCtx() + { + ensureNotReleased(); + ZstdDecompressCtx ctx = decompressCtxPool.poll(); + if (ctx == null) + ctx = new ZstdDecompressCtx().loadDict(dictionaryForDecompression()); + return ctx; + } + + /** + * Return a context borrowed from {@link #acquireDecompressCtx()}. If this dictionary has since been + * released, the context is closed instead of pooled. + */ + public void releaseDecompressCtx(ZstdDecompressCtx ctx) + { + decompressCtxPool.offer(ctx); + if (selfRef == null || selfRef.globalCount() <= 0) + drainDecompressPool(decompressCtxPool); + } + + private static void drainCompressPool(Queue pool) + { + ZstdCompressCtx ctx; + while ((ctx = pool.poll()) != null) + { + try + { + ctx.close(); + } + catch (Exception e) + { + logger.warn("Failed to close pooled ZstdCompressCtx", e); + } + } + } + + private static void drainDecompressPool(Queue pool) + { + ZstdDecompressCtx ctx; + while ((ctx = pool.poll()) != null) + { + try + { + ctx.close(); + } + catch (Exception e) + { + logger.warn("Failed to close pooled ZstdDecompressCtx", e); + } + } + } + @Override public Ref tryRef() { @@ -216,7 +320,8 @@ public Ref initRefLazily() { if (selfRef == null) { - selfRef = new Ref<>(this, new Tidy(zstdDictCompressPerLevel, dictDecompress)); + selfRef = new Ref<>(this, new Tidy(zstdDictCompressPerLevel, dictDecompress, + compressCtxPoolPerLevel, decompressCtxPool)); } } } @@ -244,12 +349,18 @@ private static class Tidy implements RefCounted.Tidy { private final ConcurrentHashMap zstdDictCompressPerLevel; private final AtomicReference dictDecompress; + private final ConcurrentHashMap> compressCtxPoolPerLevel; + private final Queue decompressCtxPool; Tidy(ConcurrentHashMap zstdDictCompressPerLevel, - AtomicReference dictDecompress) + AtomicReference dictDecompress, + ConcurrentHashMap> compressCtxPoolPerLevel, + Queue decompressCtxPool) { this.zstdDictCompressPerLevel = zstdDictCompressPerLevel; this.dictDecompress = dictDecompress; + this.compressCtxPoolPerLevel = compressCtxPoolPerLevel; + this.decompressCtxPool = decompressCtxPool; } /** @@ -267,6 +378,13 @@ private static class Tidy implements RefCounted.Tidy @Override public void tidy() { + // Close pooled (de)compression contexts BEFORE the dictionary tables they were loaded with, so no + // context can outlive the native memory it references. + for (Queue pool : compressCtxPoolPerLevel.values()) + drainCompressPool(pool); + compressCtxPoolPerLevel.clear(); + drainDecompressPool(decompressCtxPool); + // Close all compression dictionaries // No synchronization needed - reference counting ensures exclusive access for (ZstdDictCompress compressDict : zstdDictCompressPerLevel.values()) diff --git a/src/java/org/apache/cassandra/db/compression/ZstdDictionaryTrainer.java b/src/java/org/apache/cassandra/db/compression/ZstdDictionaryTrainer.java index f4ef7308ed6d..003486ade9d6 100644 --- a/src/java/org/apache/cassandra/db/compression/ZstdDictionaryTrainer.java +++ b/src/java/org/apache/cassandra/db/compression/ZstdDictionaryTrainer.java @@ -19,10 +19,14 @@ package org.apache.cassandra.db.compression; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdDictCompress; import com.github.luben.zstd.ZstdDictTrainer; import com.google.common.annotations.VisibleForTesting; @@ -32,8 +36,10 @@ import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.db.compression.CompressionDictionary.DictId; import org.apache.cassandra.db.compression.CompressionDictionary.Kind; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.AsyncFuture; import org.apache.cassandra.utils.concurrent.Future; @@ -57,10 +63,13 @@ public class ZstdDictionaryTrainer implements ICompressionDictionaryTrainer private volatile Consumer dictionaryTrainedListener; // TODO: manage the samples in this class for auto-train (follow-up). The ZstdDictTrainer cannot be re-used for multiple training runs. private volatile ZstdDictTrainer zstdTrainer; + // Retained copies of every sample fed to the ZstdDictTrainer, so the trained dictionary can be tagged with + // the compression ratio it achieves on these samples. This ratio is used in auto-training later on as well. + // Bounded by maxTotalSampleSize; cleared once training finishes. + private final List trainingSamples = Collections.synchronizedList(new ArrayList<>()); private volatile boolean closed = false; private volatile TrainingStatus currentTrainingStatus; private volatile String failureMessage; - public ZstdDictionaryTrainer(String keyspaceName, String tableName, int compressionLevel) { this.keyspaceName = keyspaceName; @@ -85,6 +94,8 @@ public void addSample(ByteBuffer sample) // Update the totalSampleSize and sampleCount if the sample is added totalSampleSize.addAndGet(sampleBytes.length); sampleCount.incrementAndGet(); + // Keep the same bytes so we can measure the dictionary's ratio on the sample after training. + trainingSamples.add(sampleBytes); } } @@ -120,7 +131,7 @@ public CompressionDictionary trainDictionary(boolean force) currentTrainingStatus = TrainingStatus.COMPLETED; logger.debug("New dictionary is trained with {}", dictId); int checksum = CompressionDictionary.calculateChecksum((byte) dictId.kind.ordinal(), dictId.id, dictBytes); - CompressionDictionary dictionary = Kind.ZSTD.createDictionary(dictId, dictBytes, checksum); + CompressionDictionary dictionary = Kind.ZSTD.createDictionary(dictId, dictBytes, checksum, FBUtilities.now()); notifyDictionaryTrainedListener(dictionary); return dictionary; } @@ -132,6 +143,30 @@ public CompressionDictionary trainDictionary(boolean force) } } + @Override + public double computeSampleCompressionRatio(byte[] dictBytes) + { + long uncompressed = 0; + long compressed = 0; + ZstdDictCompress dictCompress = new ZstdDictCompress(dictBytes, compressionLevel); + try + { + synchronized (trainingSamples) + { + for (byte[] sample : trainingSamples) + { + uncompressed += sample.length; + compressed += Zstd.compress(sample, dictCompress).length; + } + } + } + finally + { + dictCompress.close(); + } + return uncompressed > 0 ? (double) compressed / uncompressed : MetadataCollector.NO_COMPRESSION_RATIO; + } + @Override public Future trainDictionaryAsync(boolean force) { @@ -321,6 +356,7 @@ public void reset(CompressionDictionaryTrainingConfig trainingConfig) { totalSampleSize.set(0); sampleCount.set(0); + trainingSamples.clear(); try { zstdTrainer = new ZstdDictTrainer(trainingConfig.maxTotalSampleSize, trainingConfig.maxDictionarySize, compressionLevel); diff --git a/src/java/org/apache/cassandra/db/virtual/AbstractLazyVirtualTable.java b/src/java/org/apache/cassandra/db/virtual/AbstractLazyVirtualTable.java index 1924cb6f8001..71066209d575 100644 --- a/src/java/org/apache/cassandra/db/virtual/AbstractLazyVirtualTable.java +++ b/src/java/org/apache/cassandra/db/virtual/AbstractLazyVirtualTable.java @@ -707,12 +707,16 @@ protected AbstractLazyVirtualTable(TableMetadata metadata, OnTimeout onTimeout, if (!metadata.isVirtual()) throw new IllegalArgumentException("Cannot instantiate a non-virtual table"); - if (!metadata.keyspace.startsWith(SchemaConstants.ACCORD_KEYSPACE_NAME)) + if (!metadata.keyspace.startsWith(SchemaConstants.ACCORD_KEYSPACE_NAME) + && !metadata.keyspace.equals(SchemaConstants.VIRTUAL_VIEWS_REMOTE)) { // NOTE: there is nothing stopping other use cases from using this facility, but there was // feedback on the ticket questioning the reliance on Accord integration tests for validating the API. // If another use case wishes to use the facility, simply satisfy reviewers in this regard. See PR #4373 for details. - throw new IllegalArgumentException("This facility is only currently supported by Accord keyspaces"); + // system_views_remote is such a use case, and it is covered outside Accord by + // CompressionDictionaryAutoTrainingVirtualTableTest. + throw new IllegalArgumentException("This facility is only currently supported by Accord keyspaces " + + "and " + SchemaConstants.VIRTUAL_VIEWS_REMOTE); } this.metadata = metadata; diff --git a/src/java/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTable.java b/src/java/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTable.java new file mode 100644 index 000000000000..e1fd6dd9f469 --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTable.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.virtual; + +import java.util.Date; + +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingHistory; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingHistory.Entry; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.ReversedType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.schema.TableMetadata; + +/** + * Exposes the auto-training adoption decisions this node has made, as recorded by + * {@link CompressionDictionaryAutoTrainingHistory}: when a candidate dictionary was trained, for which table, + * how it compared against the dictionary already in use, and whether it was adopted. + *

+ * Node-local and held in memory only, like the other {@code system_views} tables: a restart clears it, and only + * the node that ran the training has rows for it. + */ +final class CompressionDictionaryAutoTrainingTable extends AbstractVirtualTable +{ + static final String TABLE_NAME = "compression_dictionary_auto_training"; + private static final String TABLE_COMMENT = "Auto-training decisions for compression dictionaries on this node"; + + static final String KEYSPACE_NAME = "keyspace_name"; + static final String TABLE_NAME_COLUMN = "table_name"; + static final String TRAINED_AT = "trained_at"; + static final String NODE = "node"; + static final String KIND = "kind"; + static final String BASELINE_RATIO = "baseline_ratio"; + static final String CANDIDATE_RATIO = "candidate_ratio"; + static final String IMPROVEMENT = "improvement"; + static final String THRESHOLD = "threshold"; + static final String PROMOTED = "promoted"; + + CompressionDictionaryAutoTrainingTable(String keyspace) + { + super(TableMetadata.builder(keyspace, TABLE_NAME) + .comment(TABLE_COMMENT) + .kind(TableMetadata.Kind.VIRTUAL) + .partitioner(new LocalPartitioner(UTF8Type.instance)) + .addPartitionKeyColumn(KEYSPACE_NAME, UTF8Type.instance) + .addClusteringColumn(TABLE_NAME_COLUMN, UTF8Type.instance) + // reversed so the most recent training for a table comes first + .addClusteringColumn(TRAINED_AT, ReversedType.getInstance(TimestampType.instance)) + .addRegularColumn(NODE, InetAddressType.instance) + .addRegularColumn(KIND, UTF8Type.instance) + .addRegularColumn(BASELINE_RATIO, DoubleType.instance) + .addRegularColumn(CANDIDATE_RATIO, DoubleType.instance) + .addRegularColumn(IMPROVEMENT, DoubleType.instance) + .addRegularColumn(THRESHOLD, DoubleType.instance) + .addRegularColumn(PROMOTED, BooleanType.instance) + .build()); + } + + @Override + public DataSet data() + { + SimpleDataSet result = new SimpleDataSet(metadata()); + + for (Entry entry : CompressionDictionaryAutoTrainingHistory.instance.entries()) + { + result.row(entry.keyspaceName, entry.tableName, new Date(entry.timestampMillis)) + .column(NODE, entry.node.getAddress()) + .column(KIND, entry.kind == null ? null : entry.kind.name()) + .column(BASELINE_RATIO, entry.baselineRatio) + .column(CANDIDATE_RATIO, entry.candidateRatio) + .column(IMPROVEMENT, entry.improvement) + .column(THRESHOLD, entry.threshold) + .column(PROMOTED, entry.promoted); + } + + return result; + } +} diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java index 9a80d1e9b930..f97721aa903f 100644 --- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java +++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java @@ -40,6 +40,7 @@ private SystemViewsKeyspace() .add(new SchemaCommentsTable(VIRTUAL_VIEWS)) .add(new SchemaSecurityLabelsTable(VIRTUAL_VIEWS)) .add(new SSTableTasksTable(VIRTUAL_VIEWS)) + .add(new CompressionDictionaryAutoTrainingTable(VIRTUAL_VIEWS)) // Fully backward/forward compatible with the legace ThreadPoolsTable under the same "system_views.thread_pools" name. .add(CollectionVirtualTableAdapter.create(VIRTUAL_VIEWS, "thread_pools", diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsRemoteKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsRemoteKeyspace.java new file mode 100644 index 000000000000..dfab3de2d91c --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsRemoteKeyspace.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.virtual; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.schema.SchemaConstants; + +/** + * Cluster-wide counterparts of {@link SystemViewsKeyspace} tables, so a node-local view can be read for every node + * from a single coordinator. Each table here carries the same columns as the one it mirrors, prefixed with a + * {@code node_id} partition key; {@link RemoteToLocalVirtualTable} turns a read of one {@code node_id} into a read + * of the local table on that node. + *

+ * Only tables listed in {@link #REMOTED_TABLES} are mirrored. The wrapper cannot model every shape - a table needs + * a single partition key column and no complex or static columns - so tables opt in rather than out. + */ +public class SystemViewsRemoteKeyspace extends RemoteToLocalVirtualKeyspace +{ + private static final ImmutableSet REMOTED_TABLES = + ImmutableSet.of(CompressionDictionaryAutoTrainingTable.TABLE_NAME); + + public static final SystemViewsRemoteKeyspace instance = + new SystemViewsRemoteKeyspace(SchemaConstants.VIRTUAL_VIEWS_REMOTE, SystemViewsKeyspace.instance); + + public SystemViewsRemoteKeyspace(String name, VirtualKeyspace wrap) + { + super(name, wrap, vt -> REMOTED_TABLES.contains(vt.name())); + } +} diff --git a/src/java/org/apache/cassandra/io/compress/IDictionaryCompressor.java b/src/java/org/apache/cassandra/io/compress/IDictionaryCompressor.java index 33f6b5cdad75..ec86ff4affcc 100644 --- a/src/java/org/apache/cassandra/io/compress/IDictionaryCompressor.java +++ b/src/java/org/apache/cassandra/io/compress/IDictionaryCompressor.java @@ -42,6 +42,26 @@ public interface IDictionaryCompressor // that once we train a dictionary for given table, then we can train again after at least 1 hour. String DEFAULT_TRAINING_MIN_FREQUENCY = "0m"; + String AUTO_TRAINING_ENABLED = "auto_training_enabled"; + + /** + * For tables using {@code TimeWindowCompactionStrategy}, the maximum number of TWCS time windows, counted back + * from the newest, that auto-training may sample from. The resolver walks windows newest-first and stops as soon + * as it has gathered enough sample data to train on, but never reaches back more than this many windows. The + * default of 1 restricts sampling to the newest window only (strongest recency bias); raise it to allow training + * to draw on older windows when the newest one does not hold enough data on its own. + */ + String AUTO_TRAINING_TWCS_MAX_WINDOWS = "auto_training_twcs_max_windows"; + String DEFAULT_AUTO_TRAINING_TWCS_MAX_WINDOWS_VALUE = "1"; + + /** + * Minimum relative improvement in the compression ratio required to adopt a freshly trained + * dictionary over the current one (e.g., 0.15 means the candidate must compress the training + * sample at least 15% better). Set to 0 to adopt on any improvement. + */ + String AUTO_TRAINING_IMPROVEMENT_THRESHOLD_NAME = "auto_training_improvement_threshold"; + String DEFAULT_AUTO_TRAINING_IMPROVEMENT_THRESHOLD_VALUE = "0.15"; + /** * Returns a compressor instance configured with the specified compression dictionary. *
diff --git a/src/java/org/apache/cassandra/io/compress/ZstdCompressorBase.java b/src/java/org/apache/cassandra/io/compress/ZstdCompressorBase.java index a5cd14647804..3feb801b63a3 100644 --- a/src/java/org/apache/cassandra/io/compress/ZstdCompressorBase.java +++ b/src/java/org/apache/cassandra/io/compress/ZstdCompressorBase.java @@ -22,9 +22,13 @@ import java.nio.ByteBuffer; import java.util.Collections; import java.util.Map; +import java.util.Queue; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdCompressCtx; +import com.github.luben.zstd.ZstdDecompressCtx; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; @@ -49,6 +53,15 @@ public abstract class ZstdCompressorBase implements ICompressor private final Set recommendedUses; private final Set supportedOptions; + // Reusable native (de)compression contexts, pooled and borrowed per chunk. + // + // The static Zstd.compress / Zstd.decompress / Zstd.decompressByteArray helpers each allocate and free a fresh + // native ZSTD_CCtx / ZSTD_DCtx on EVERY call - i.e. once per chunk. At small chunk sizes that fixed per-chunk + // cost dominates, and it scales with chunk count rather than with data size. We keep a pool of contexts + // instead, each configured once, and reuse them across chunks. + private final Queue compressCtxPool = new ConcurrentLinkedQueue<>(); + private final Queue decompressCtxPool = new ConcurrentLinkedQueue<>(); + protected ZstdCompressorBase(int compressionLevel, Set supportedOptions) { this.compressionLevel = compressionLevel; @@ -108,21 +121,33 @@ public Set supportedOptions() public int uncompress(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) throws IOException { - long dsz; + ZstdDecompressCtx ctx = acquireDecompressCtx(); + boolean ok = false; try { - dsz = Zstd.decompressByteArray(output, outputOffset, output.length - outputOffset, - input, inputOffset, inputLength); + int dsz = ctx.decompressByteArray(output, outputOffset, output.length - outputOffset, + input, inputOffset, inputLength); + if (Zstd.isError(dsz)) + throw new IOException("Decompression failed due to " + Zstd.getErrorName(dsz)); + ok = true; + return dsz; + } + catch (IOException e) + { + throw e; } catch (Exception e) { throw new IOException("Decompression failed", e); } - - if (Zstd.isError(dsz)) - throw new IOException("Decompression failed due to " + Zstd.getErrorName(dsz)); - - return (int) dsz; + finally + { + // success -> return to pool; failure -> close, so a context that errored is never reused + if (ok) + decompressCtxPool.offer(ctx); + else + ctx.close(); + } } /** @@ -135,13 +160,28 @@ public int uncompress(byte[] input, int inputOffset, int inputLength, byte[] out @Override public void uncompress(ByteBuffer input, ByteBuffer output) throws IOException { + ZstdDecompressCtx ctx = acquireDecompressCtx(); + boolean ok = false; try { - Zstd.decompress(output, input); - } catch (Exception e) + // Zstd compressors expect only direct bytebuffer. See preferredBufferType and supports. + int decompressedSize = ctx.decompressDirectByteBuffer(output, output.position(), output.limit() - output.position(), + input, input.position(), input.limit() - input.position()); + output.position(output.position() + decompressedSize); + input.position(input.limit()); + ok = true; + } + catch (Exception e) { throw new IOException("Decompression failed", e); } + finally + { + if (ok) + decompressCtxPool.offer(ctx); + else + ctx.close(); + } } /** @@ -154,13 +194,60 @@ public void uncompress(ByteBuffer input, ByteBuffer output) throws IOException @Override public void compress(ByteBuffer input, ByteBuffer output) throws IOException { + ZstdCompressCtx ctx = acquireCompressCtx(); + boolean ok = false; try { - Zstd.compress(output, input, compressionLevel(), ENABLE_CHECKSUM_FLAG); - } catch (Exception e) + // Zstd compressors expect only direct bytebuffer. See preferredBufferType and supports. + int compressedSize = ctx.compressDirectByteBuffer(output, output.position(), output.limit() - output.position(), + input, input.position(), input.limit() - input.position()); + output.position(output.position() + compressedSize); + input.position(input.limit()); + ok = true; + } + catch (Exception e) { throw new IOException("Compression failed", e); } + finally + { + if (ok) + compressCtxPool.offer(ctx); + else + ctx.close(); + } + } + + // ---- pooled native context management (see the compressCtxPool field comment) ---- + + private ZstdCompressCtx acquireCompressCtx() + { + ZstdCompressCtx ctx = compressCtxPool.poll(); + if (ctx == null) + { + // Level and checksum are set once here, matching what the static Zstd.compress helper did per call, so + // the frames written are identical to those written before contexts were pooled. + ctx = new ZstdCompressCtx().setLevel(compressionLevel()).setChecksum(ENABLE_CHECKSUM_FLAG); + } + return ctx; + } + + private ZstdDecompressCtx acquireDecompressCtx() + { + ZstdDecompressCtx ctx = decompressCtxPool.poll(); + return ctx == null ? new ZstdDecompressCtx() : ctx; + } + + @VisibleForTesting + int pooledCompressContexts() + { + return compressCtxPool.size(); + } + + @VisibleForTesting + int pooledDecompressContexts() + { + return decompressCtxPool.size(); } /** diff --git a/src/java/org/apache/cassandra/io/compress/ZstdDictionaryCompressor.java b/src/java/org/apache/cassandra/io/compress/ZstdDictionaryCompressor.java index 39fb09cd1882..7c8acfb9f35d 100644 --- a/src/java/org/apache/cassandra/io/compress/ZstdDictionaryCompressor.java +++ b/src/java/org/apache/cassandra/io/compress/ZstdDictionaryCompressor.java @@ -31,6 +31,8 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdCompressCtx; +import com.github.luben.zstd.ZstdDecompressCtx; import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.concurrent.ImmediateExecutor; @@ -51,11 +53,10 @@ public class ZstdDictionaryCompressor extends ZstdCompressorBase implements ICom .removalListener((ZstdCompressionDictionary dictionary, ZstdDictionaryCompressor compressor, RemovalCause cause) -> { - // Release dictionary reference when compressor is evicted from cache + // Release dictionary reference when compressor is evicted from cache. The dictionary's own Tidy + // closes any pooled native (de)compression contexts once this was the last reference. if (compressor != null && compressor.dictionaryRef != null) - { compressor.dictionaryRef.release(); - } }) .executor(ImmediateExecutor.INSTANCE) .build(); @@ -82,6 +83,10 @@ public static ZstdDictionaryCompressor create(Map options) CompressionDictionaryTrainingConfig.getMaxDictionarySize(options); CompressionDictionaryTrainingConfig.getMaxTotalSampleSize(options); CompressionDictionaryTrainingConfig.getMinTrainingFrequency(options); + CompressionDictionaryTrainingConfig.isAutoTrainingEnabled(options); + CompressionDictionaryTrainingConfig.getAutoTrainingImprovementThreshold(options); + CompressionDictionaryTrainingConfig.getAutoTrainingTwcsMaxWindows(options); + return getOrCreate(level, null); } @@ -119,7 +124,10 @@ private ZstdDictionaryCompressor(int level, ZstdCompressionDictionary dictionary super(level, Set.of(COMPRESSION_LEVEL_OPTION_NAME, TRAINING_MAX_DICTIONARY_SIZE_PARAMETER_NAME, TRAINING_MAX_TOTAL_SAMPLE_SIZE_PARAMETER_NAME, - TRAINING_MIN_FREQUENCY_PARAMETER_NAME)); + TRAINING_MIN_FREQUENCY_PARAMETER_NAME, + AUTO_TRAINING_ENABLED, + AUTO_TRAINING_TWCS_MAX_WINDOWS, + AUTO_TRAINING_IMPROVEMENT_THRESHOLD_NAME)); this.dictionary = dictionary; this.dictionaryRef = dictionaryRef; } @@ -145,22 +153,39 @@ public int uncompress(byte[] input, int inputOffset, int inputLength, byte[] out return super.uncompress(input, inputOffset, inputLength, output, outputOffset); } - int dsz; + ZstdDecompressCtx ctx = null; + boolean ok = false; try { - dsz = (int) Zstd.decompressFastDict(output, outputOffset, - input, inputOffset, inputLength, - dictionary.dictionaryForDecompression()); + // Reuse a pooled context (dictionary loaded once) rather than the static one-shot, which allocates a + // fresh ZSTD_DCtx per call. + ctx = dictionary.acquireDecompressCtx(); + int dsz = ctx.decompressByteArray(output, outputOffset, output.length - outputOffset, + input, inputOffset, inputLength); + if (Zstd.isError(dsz)) + throw new IOException("Decompression failed due to " + Zstd.getErrorName(dsz)); + ok = true; + return dsz; + } + catch (IOException e) + { + throw e; } catch (Exception e) { throw new IOException("Decompression failed", e); } - - if (Zstd.isError(dsz)) - throw new IOException("Decompression failed due to " + Zstd.getErrorName(dsz)); - - return dsz; + finally + { + // success -> return to pool; failure -> close, so a context that errored is never reused + if (ctx != null) + { + if (ok) + dictionary.releaseDecompressCtx(ctx); + else + ctx.close(); + } + } } @Override @@ -172,19 +197,34 @@ public void uncompress(ByteBuffer input, ByteBuffer output) throws IOException return; } + ZstdDecompressCtx ctx = null; + boolean ok = false; try { - // Zstd compressors expect only direct bytebuffer. See ZstdCompressorBase.preferredBufferType and supports - int decompressedSize = (int) Zstd.decompressDirectByteBufferFastDict(output, output.position(), output.limit() - output.position(), - input, input.position(), input.limit() - input.position(), - dictionary.dictionaryForDecompression()); + // Zstd compressors expect only direct bytebuffer. See ZstdCompressorBase.preferredBufferType and supports. + // The context carries the dictionary (loaded once) and is reused across chunks. + ctx = dictionary.acquireDecompressCtx(); + int decompressedSize = ctx.decompressDirectByteBuffer(output, output.position(), output.limit() - output.position(), + input, input.position(), input.limit() - input.position()); output.position(output.position() + decompressedSize); input.position(input.limit()); + ok = true; } catch (Exception e) { throw new IOException("Decompression failed", e); } + finally + { + // success -> return to pool; failure -> close, so a context that errored is never reused + if (ctx != null) + { + if (ok) + dictionary.releaseDecompressCtx(ctx); + else + ctx.close(); + } + } } @Override @@ -196,19 +236,34 @@ public void compress(ByteBuffer input, ByteBuffer output) throws IOException return; } + ZstdCompressCtx ctx = null; + boolean ok = false; try { - // Zstd compressors expect only direct bytebuffer. See ZstdCompressorBase.preferredBufferType and supports - int compressedSize = (int) Zstd.compressDirectByteBufferFastDict(output, output.position(), output.limit() - output.position(), - input, input.position(), input.limit() - input.position(), - dictionary.dictionaryForCompression(compressionLevel())); + // Zstd compressors expect only direct bytebuffer. See ZstdCompressorBase.preferredBufferType and supports. + // The context carries the dictionary (loaded once) and is reused across chunks. + ctx = dictionary.acquireCompressCtx(compressionLevel()); + int compressedSize = ctx.compressDirectByteBuffer(output, output.position(), output.limit() - output.position(), + input, input.position(), input.limit() - input.position()); output.position(output.position() + compressedSize); input.position(input.limit()); + ok = true; } catch (Exception e) { throw new IOException("Compression failed", e); } + finally + { + // success -> return to pool; failure -> close, so a context that errored is never reused + if (ctx != null) + { + if (ok) + dictionary.releaseCompressCtx(compressionLevel(), ctx); + else + ctx.close(); + } + } } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java index ada413537d15..1f7612249633 100644 --- a/src/java/org/apache/cassandra/schema/SchemaConstants.java +++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java @@ -58,6 +58,7 @@ public final class SchemaConstants public static final String VIRTUAL_METRICS = "system_metrics"; public static final String VIRTUAL_ACCORD_DEBUG = "system_accord_debug"; public static final String VIRTUAL_ACCORD_DEBUG_REMOTE = "system_accord_debug_remote"; + public static final String VIRTUAL_VIEWS_REMOTE = "system_views_remote"; public static final String DUMMY_KEYSPACE_OR_TABLE_NAME = "--dummy--"; @@ -67,7 +68,8 @@ public final class SchemaConstants /* virtual table system keyspace names */ public static final Set VIRTUAL_SYSTEM_KEYSPACE_NAMES = - ImmutableSet.of(VIRTUAL_SCHEMA, VIRTUAL_VIEWS, VIRTUAL_METRICS, VIRTUAL_ACCORD_DEBUG, VIRTUAL_ACCORD_DEBUG_REMOTE); + ImmutableSet.of(VIRTUAL_SCHEMA, VIRTUAL_VIEWS, VIRTUAL_VIEWS_REMOTE, VIRTUAL_METRICS, + VIRTUAL_ACCORD_DEBUG, VIRTUAL_ACCORD_DEBUG_REMOTE); /* replicate system keyspace names (the ones with a "true" replication strategy) */ public static final Set REPLICATED_SYSTEM_KEYSPACE_NAMES = diff --git a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java index 15f8ca6fbee5..a0bd837c486f 100644 --- a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java +++ b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java @@ -440,7 +440,7 @@ public static void storeCompressionDictionary(String keyspaceName, dict.length, dictionary.checksum()); noThrow(fmtQuery, - () -> QueryProcessor.process(fmtQuery, ConsistencyLevel.ONE, + () -> QueryProcessor.process(fmtQuery, ConsistencyLevel.QUORUM, List.of(ByteBuffer.wrap(dict), ByteBufferUtil.bytes(dictionary.createdAt().toEpochMilli())))); } @@ -460,10 +460,19 @@ public static CompressionDictionary retrieveLatestCompressionDictionary(String k String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId); try { - return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); + UntypedResultSet result = QueryProcessor.execute(fmtQuery, ConsistencyLevel.QUORUM); + // No row means this table has no dictionary stored, which is an ordinary state and not a read + // failure. It must not reach the catch below: one() throws on an empty result, so the warning + // would fire on every call for every such table, drowning out the failures it exists to surface. + if (result.isEmpty()) + return null; + return CompressionDictionary.createFromRow(result.one()); } catch (Exception e) { + // Never swallow this silently: callers treat null as "no dictionary" and carry on with a stale + // one, so an unavailable replica would otherwise look identical to a table that has none. + logger.warn("Could not read the latest compression dictionary for {}.{}", keyspaceName, tableName, e); return null; } } @@ -485,10 +494,14 @@ public static LightweightCompressionDictionary retrieveLightweightLatestCompress String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId); try { - return CompressionDictionary.createFromRowLightweight(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); + UntypedResultSet result = QueryProcessor.execute(fmtQuery, ConsistencyLevel.QUORUM); + if (result.isEmpty()) + return null; + return CompressionDictionary.createFromRowLightweight(result.one()); } catch (Exception e) { + logger.warn("Could not read the latest compression dictionary metadata for {}.{}", keyspaceName, tableName, e); return null; } } @@ -511,10 +524,14 @@ public static CompressionDictionary retrieveCompressionDictionary(String keyspac String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId, dictionaryId); try { - return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); + UntypedResultSet result = QueryProcessor.execute(fmtQuery, ConsistencyLevel.QUORUM); + if (result.isEmpty()) + return null; + return CompressionDictionary.createFromRow(result.one()); } catch (Exception e) { + logger.warn("Could not read compression dictionary {} for {}.{}", dictionaryId, keyspaceName, tableName, e); return null; } } @@ -589,7 +606,7 @@ public static void clearOrphanedCompressionDictionaries() orphanedDict.tableName, orphanedDict.tableId, orphanedDict.dictId.id), - ConsistencyLevel.ONE); + ConsistencyLevel.QUORUM); } catch (Exception e) { @@ -604,7 +621,7 @@ private static List retrieveLightweightCompres { try { - UntypedResultSet result = QueryProcessor.execute(query, ConsistencyLevel.ONE); + UntypedResultSet result = QueryProcessor.execute(query, ConsistencyLevel.QUORUM); if (result.isEmpty()) return Collections.emptyList(); List dictionaries = new ArrayList<>(); diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java index 3e3a3144c92d..9e9a73b4fcc0 100644 --- a/src/java/org/apache/cassandra/service/CassandraDaemon.java +++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java @@ -65,12 +65,14 @@ import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.SystemKeyspaceMigrator41; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager; import org.apache.cassandra.db.virtual.AccordDebugKeyspace; import org.apache.cassandra.db.virtual.AccordDebugRemoteKeyspace; import org.apache.cassandra.db.virtual.ExceptionsTable; import org.apache.cassandra.db.virtual.LogMessagesTable; import org.apache.cassandra.db.virtual.SlowQueriesTable; import org.apache.cassandra.db.virtual.SystemViewsKeyspace; +import org.apache.cassandra.db.virtual.SystemViewsRemoteKeyspace; import org.apache.cassandra.db.virtual.VirtualKeyspace; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace; @@ -444,6 +446,9 @@ protected void setup() DatabaseDescriptor.getReadRpcTimeout(NANOSECONDS), NANOSECONDS); + // periodically retrain compression dictionaries, adopting a freshly trained one only when it compresses better + CompressionDictionaryAutoTrainingManager.instance.start(); + initializeClientTransports(); // Ensure you've registered all caches during startup you want pre-warmed before this call -> be wary of adding @@ -642,6 +647,7 @@ public void setupVirtualKeyspaces() { VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance); VirtualKeyspaceRegistry.instance.register(SystemViewsKeyspace.instance); + VirtualKeyspaceRegistry.instance.register(SystemViewsRemoteKeyspace.instance); VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(VIRTUAL_METRICS, createMetricsKeyspaceTables())); if (DatabaseDescriptor.getAccord().enable_virtual_debug_only_keyspace) diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index a897cba85e1c..5a8a156e64d7 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -108,6 +108,7 @@ import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.compression.CompressionDictionary.LightweightCompressionDictionary; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager; import org.apache.cassandra.db.compression.CompressionDictionaryDetailsTabularData; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; @@ -3888,6 +3889,7 @@ protected synchronized void drain(boolean isFinalShutdown) throws IOException, I } SnapshotManager.instance.close(); + CompressionDictionaryAutoTrainingManager.instance.close(); HintsService.instance.pauseDispatch(); if (daemon != null) diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java index 98ea380b63cc..c973adca0b0c 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java @@ -85,6 +85,8 @@ import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionLogger; import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager; +import org.apache.cassandra.db.compression.CompressionDictionaryScheduler; import org.apache.cassandra.db.memtable.AbstractAllocatorMemtable; import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.distributed.Cluster; @@ -935,6 +937,7 @@ else if (cluster instanceof Cluster) ActiveRepairService.instance().start(); StreamManager.instance.start(); + CompressionDictionaryAutoTrainingManager.instance.start(); PaxosState.startAutoRepairs(); StorageService.instance.doAutoRepairSetup(); CassandraDaemon.getInstanceForTesting().completeSetup(); @@ -974,7 +977,7 @@ public Future shutdown(boolean runOnExitThreads, boolean shutdownMessaging Throwable error = null; inInstancelogger.warn("Shutting down in thread {}", Thread.currentThread().getName()); - error = parallelRun(error, executor, SnapshotManager.instance::close); + error = parallelRun(error, executor, SnapshotManager.instance::close, CompressionDictionaryAutoTrainingManager.instance::close); CompactionManager.instance.forceShutdown(); @@ -1030,6 +1033,7 @@ public Future shutdown(boolean runOnExitThreads, boolean shutdownMessaging error = parallelRun(error, executor, shutdownBatchlogAndHints, () -> CompactionLogger.shutdownNowAndWait(1L, MINUTES), + () -> CompressionDictionaryScheduler.shutdownNowAndWait(1L, MINUTES), () -> AuthCache.shutdownAllAndWait(1L, MINUTES), () -> Sampler.shutdownNowAndWait(1L, MINUTES), NettyStreamingChannel::shutdown, diff --git a/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingDistributedTest.java b/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingDistributedTest.java new file mode 100644 index 000000000000..398e8908728b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingDistributedTest.java @@ -0,0 +1,478 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.compression.CompressionDictionary; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingHistory; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.io.compress.CompressionMetadata; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.SystemDistributedKeyspace; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.awaitility.Awaitility.await; + +/** + * Distributed (in-JVM, 3-node) end-to-end test of compression-dictionary auto-training. + *

+ * Mirrors the single-node recency integration test, but exercises the full cluster path: every node boots with + * auto-training enabled, one node's scheduled auto-trainer (the CMS leader) adopts a better dictionary, and the + * adopted dictionary is broadcast to the other nodes so that new writes on every node are compressed with it. + *

+ * Flow: + *

    + *
  1. 3-node cluster, RF=3, {@code compression_dictionary_auto_training_enabled=true} on each node;
  2. + *
  3. a TWCS, dictionary-compressed, auto-training-enabled table;
  4. + *
  5. load window 1 (vocabulary A), flush every node, and hand-train the first ("latest") dictionary;
  6. + *
  7. load window 2 with a disjoint vocabulary B (the drift), flush every node;
  8. + *
  9. wait for the scheduled auto-trainer to adopt a new dictionary (persisted to {@code system_distributed});
  10. + *
  11. wait for that dictionary to propagate to every node's current-dictionary cache;
  12. + *
  13. write more data and flush on every node, then assert every node's fresh SSTables are compressed with the + * adopted dictionary id - i.e. new writes cluster-wide use the new dictionary;
  14. + *
  15. read the decision back from {@code system_views.compression_dictionary_auto_training} on the node that + * trained, and from {@code system_views_remote.compression_dictionary_auto_training} on every node, which + * answers for the whole cluster.
  16. + *
+ * Cadence note: the auto-training interval/initial-delay are minute-granular (minimum 60s), so this test genuinely + * waits for a scheduled cycle rather than triggering one. + */ +public class CompressionDictionaryAutoTrainingDistributedTest extends TestBaseImpl +{ + private static final String TABLE = "recency_tbl"; + + /** The node-local view: only the node that ran the training has rows. */ + private static final String LOCAL_DECISIONS = + "SELECT node, kind, baseline_ratio, candidate_ratio, improvement, threshold, promoted " + + "FROM system_views.compression_dictionary_auto_training WHERE keyspace_name = ? AND table_name = ?"; + + /** The cluster-wide view: any coordinator answers for every node, each row tagged with its node_id. */ + private static final String REMOTE_DECISIONS = + "SELECT node_id, node, keyspace_name, table_name, kind, improvement, threshold, promoted " + + "FROM system_views_remote.compression_dictionary_auto_training"; + + private static final String REMOTE_DECISIONS_OF_NODE = REMOTE_DECISIONS + " WHERE node_id = ?"; + + /** Matches {@code auto_training_improvement_threshold} in the table definition below. */ + private static final double THRESHOLD = 0.05; + private static final long MICROS_PER_DAY = 86_400L * 1_000_000L; + private static final int ROWS_PER_WINDOW = 250; + private static final int VOCAB_SIZE = 256; + private static final int PHRASE_LEN = 32; + private static final int PHRASES_PER_ROW = 32; // ~1KiB per value + + @Test + public void newWritesUseAutoTrainedDictionaryAcrossCluster() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(c -> c.set("compression_dictionary_auto_training_enabled", true) + .set("compression_dictionary_auto_training_initial_delay", "1m") + .set("compression_dictionary_auto_training_interval", "1m") + // flush with the table's own (dictionary) compressor at its 4KiB chunk length, so + // flushed SSTables have enough small chunks to sample for training + .set("flush_compression", "table")) + .start(), + 3)) + { + String ks = KEYSPACE; + + // 1. auto-training must be enabled on every node + for (int n = 1; n <= 3; n++) + Assert.assertTrue("auto-training must be enabled on node " + n, + cluster.get(n).callOnInstance(DatabaseDescriptor::getCompressionDictionaryAutoTrainingEnabled)); + + // 2. TWCS, dictionary-compressed, auto-training-enabled table (small sample sizes so training is quick) + cluster.schemaChange(createTable(ks)); + + // 3. window 1 (vocabulary A) -> flush everywhere -> hand-train the first ("latest") dictionary. + // This trains on node 1 directly (equivalent to `nodetool compressiondictionary train`, but without + // needing a JMX feature) and persists+broadcasts the dictionary cluster-wide. + writeWindow(cluster, 1, vocabulary(1)); + cluster.forEach(i -> i.flush(ks)); + cluster.get(1).runOnInstance(() -> + Keyspace.open(ks).getColumnFamilyStore(TABLE).compressionDictionaryManager().train(true, Collections.emptyMap())); + await("hand-trained dictionary becomes available on node 1") + .atMost(1, TimeUnit.MINUTES).pollInterval(1, TimeUnit.SECONDS) + .until(() -> currentDictId(cluster.get(1), ks) > 0); + + long firstDictId = currentDictId(cluster.get(1), ks); + Assert.assertTrue("a hand-trained dictionary must exist, was " + firstDictId, firstDictId > 0); + + // 4. window 2 (disjoint vocabulary B = the drift) -> flush everywhere + writeWindow(cluster, 2, vocabulary(2)); + cluster.forEach(i -> i.flush(ks)); + + // 5. wait for the scheduled auto-trainer (running on the CMS leader) to adopt and persist a new dictionary + await("auto-training adopts a new dictionary") + .atMost(4, TimeUnit.MINUTES).pollInterval(5, TimeUnit.SECONDS) + .until(() -> { + long latest = latestPersistedDictId(cluster.get(1), ks); + return latest > 0 && latest != firstDictId; + }); + long adoptedDictId = latestPersistedDictId(cluster.get(1), ks); + Assert.assertNotEquals("a new dictionary must have been adopted", firstDictId, adoptedDictId); + + // 6. the adopted dictionary must propagate to every node's current-dictionary cache + for (int n = 1; n <= 3; n++) + { + IInvokableInstance instance = cluster.get(n); + await("node " + n + " picks up the adopted dictionary") + .atMost(1, TimeUnit.MINUTES).pollInterval(2, TimeUnit.SECONDS) + .until(() -> currentDictId(instance, ks) == adoptedDictId); + } + + // 7. new writes on every node must be compressed with the adopted dictionary + writeWindow(cluster, 3, vocabulary(2)); + cluster.forEach(i -> i.flush(ks)); + for (int n = 1; n <= 3; n++) + { + Set ids = sstableDictIds(cluster.get(n), ks); + Assert.assertTrue("node " + n + " must have SSTable(s) written with the adopted dictionary " + + adoptedDictId + ", but saw dictionary ids " + ids, + ids.contains(adoptedDictId)); + } + + // 8. the decision behind that adoption must be readable from the virtual tables + assertDecisionVisibleLocally(cluster, ks); + assertDecisionVisibleClusterWide(cluster, ks); + } + } + + /** + * Auto-training runs on exactly one node: the first CMS member, which is the lowest {@code NodeId} among + * {@link ClusterMetadata#fullCMSMemberIds()}. That set is CMS membership, not liveness, so the role moves + * only when membership changes - a reconfiguration that drops the current holder, the holder leaving the cluster, + * or a node with a lower NodeId joining the CMS. A holder that is merely down does not hand over; auto-training + * just stops until it returns or the CMS is reconfigured. + *

+ * Here the CMS is moved to the other datacenter, which drops node1 from it, and the recording of decisions must + * follow to the new holder. + */ + @Test + public void autoTrainingFollowsTheFirstCmsMember() throws Throwable + { + // 2 datacenters, 2 nodes each: datacenter1 = nodes 1,2; datacenter2 = nodes 3,4 + try (Cluster cluster = builder().withRacks(2, 1, 2) + .withConfig(c -> c.with(GOSSIP).with(NETWORK) + .set("compression_dictionary_auto_training_enabled", true) + .set("compression_dictionary_auto_training_initial_delay", "1m") + .set("compression_dictionary_auto_training_interval", "1m") + .set("flush_compression", "table")) + .start()) + { + String ks = KEYSPACE; + cluster.schemaChange("CREATE KEYSPACE " + ks + " WITH replication = " + + "{'class':'NetworkTopologyStrategy','datacenter1':2,'datacenter2':2}"); + cluster.schemaChange(createTable(ks)); + + // a baseline dictionary, so later cycles have something to compare a candidate against + writeWindow(cluster, 1, vocabulary(1)); + cluster.forEach(i -> i.flush(ks)); + cluster.get(1).runOnInstance(() -> + Keyspace.open(ks).getColumnFamilyStore(TABLE).compressionDictionaryManager().train(true, Collections.emptyMap())); + await("hand-trained dictionary becomes available on node 1") + .atMost(1, TimeUnit.MINUTES).pollInterval(1, TimeUnit.SECONDS) + .until(() -> currentDictId(cluster.get(1), ks) > 0); + + // drift, so every cycle from now on evaluates a candidate and therefore records a decision + writeWindow(cluster, 2, vocabulary(2)); + cluster.forEach(i -> i.flush(ks)); + + int firstHolder = firstCMSMember(cluster); + Assert.assertEquals("node 1 starts out as the first CMS member", 1, firstHolder); + awaitOnlyRecorder(cluster, ks, firstHolder); + + // move the CMS into the other datacenter, dropping node 1 from it + cluster.get(1).nodetoolResult("cms", "reconfigure", "datacenter2:1").asserts().success(); + + int secondHolder = firstCMSMember(cluster); + Assert.assertNotEquals("moving the CMS must hand the role to another node", firstHolder, secondHolder); + + // forget the decisions recorded under the old holder, so the next cycle speaks for itself + cluster.forEach(i -> i.runOnInstance(() -> CompressionDictionaryAutoTrainingHistory.instance.clear())); + awaitOnlyRecorder(cluster, ks, secondHolder); + } + } + + /** + * Auto-training runs on one node only (the first CMS member), so exactly one node records decisions, and its + * node-local virtual table must describe the adoption: the configured threshold, an improvement that met it, a + * candidate that compressed better than the baseline, and its own address. + */ + private static void assertDecisionVisibleLocally(Cluster cluster, String ks) + { + int trainingNode = trainingNode(cluster, ks); + Object[][] decisions = cluster.coordinator(trainingNode).execute(LOCAL_DECISIONS, ConsistencyLevel.ONE, ks, TABLE); + Object[] promoted = promotedRow(decisions, 6); + Assert.assertNotNull("node " + trainingNode + " must have recorded a promoted candidate, rows: " + + decisions.length, promoted); + + // columns: node, kind, baseline_ratio, candidate_ratio, improvement, threshold, promoted + double baselineRatio = (Double) promoted[2]; + double candidateRatio = (Double) promoted[3]; + double improvement = (Double) promoted[4]; + double threshold = (Double) promoted[5]; + + Assert.assertEquals("the trained dictionary kind must be recorded", + CompressionDictionary.Kind.ZSTD.name(), promoted[1]); + + Assert.assertEquals("threshold must be the table's configured one", THRESHOLD, threshold, 1e-9); + Assert.assertTrue("a promoted candidate must have met the threshold, improvement=" + improvement + + " threshold=" + threshold, improvement >= threshold); + Assert.assertTrue("the promoted candidate must compress better than the baseline, baseline=" + baselineRatio + + " candidate=" + candidateRatio, candidateRatio < baselineRatio); + Assert.assertEquals("the row must name the node that trained", + cluster.get(trainingNode).config().broadcastAddress().getAddress(), promoted[0]); + } + + /** + * The same decision must be readable through {@code system_views_remote} from any coordinator, including + * the nodes that did not train, and a {@code node_id} restriction must narrow the read to the node that did. + */ + private static void assertDecisionVisibleClusterWide(Cluster cluster, String ks) + { + int trainingNode = trainingNode(cluster, ks); + Object expectedAddress = cluster.get(trainingNode).config().broadcastAddress().getAddress(); + + for (int n = 1; n <= 3; n++) + { + // columns: node_id, node, keyspace_name, table_name, kind, improvement, threshold, promoted + Object[][] all = cluster.coordinator(n).execute(REMOTE_DECISIONS, ConsistencyLevel.ONE); + Object[] promoted = promotedRow(ofTable(all, ks), 7); + Assert.assertNotNull("the adoption must be visible cluster-wide from node " + n, promoted); + Assert.assertEquals("the cluster-wide row must name the node that trained", expectedAddress, promoted[1]); + Assert.assertEquals("the cluster-wide row must carry the dictionary kind", + CompressionDictionary.Kind.ZSTD.name(), promoted[4]); + + int nodeId = (Integer) promoted[0]; + Object[][] ofNode = cluster.coordinator(n).execute(REMOTE_DECISIONS_OF_NODE, ConsistencyLevel.ONE, nodeId); + Assert.assertNotNull("reading node_id " + nodeId + " alone from node " + n + " must carry the adoption", + promotedRow(ofTable(ofNode, ks), 7)); + for (Object[] row : ofNode) + Assert.assertEquals("restricting node_id must only return that node's rows", nodeId, row[0]); + } + } + + /** The single node whose local history holds decisions for the table, waiting for it to appear. */ + private static int trainingNode(Cluster cluster, String ks) + { + Set recording = new HashSet<>(); + await("exactly one node records auto-training decisions") + .atMost(1, TimeUnit.MINUTES).pollInterval(2, TimeUnit.SECONDS) + .until(() -> { + recording.clear(); + for (int n = 1; n <= 3; n++) + { + if (cluster.coordinator(n).execute(LOCAL_DECISIONS, ConsistencyLevel.ONE, ks, TABLE).length > 0) + recording.add(n); + } + return recording.size() == 1; + }); + return recording.iterator().next(); + } + + /** Rows whose {@code keyspace_name} is {@code ks} and whose table is {@link #TABLE}. */ + private static Object[][] ofTable(Object[][] rows, String ks) + { + List result = new ArrayList<>(); + for (Object[] row : rows) + { + if (ks.equals(row[2]) && TABLE.equals(row[3])) + result.add(row); + } + return result.toArray(new Object[0][]); + } + + /** The first row whose {@code promoted} column (at {@code promotedAt}) is true, or null. */ + private static Object[] promotedRow(Object[][] rows, int promotedAt) + { + for (Object[] row : rows) + { + if (Boolean.TRUE.equals(row[promotedAt])) + return row; + } + return null; + } + + + /** TWCS, dictionary-compressed, auto-training-enabled, with small sample sizes so training is quick. */ + private static String createTable(String ks) + { + return "CREATE TABLE " + ks + '.' + TABLE + " (id int PRIMARY KEY, v text) WITH compression = {" + + "'class':'ZstdDictionaryCompressor'," + + "'chunk_length_in_kb':4," + + "'auto_training_enabled':'true'," + + "'training_min_frequency':'0m'," + + "'auto_training_improvement_threshold':'" + THRESHOLD + "'," + + "'training_max_dictionary_size':'8KiB'," + + "'training_max_total_sample_size':'64KiB'} " + + "AND compaction = {'class':'TimeWindowCompactionStrategy'," + + "'compaction_window_unit':'DAYS','compaction_window_size':1}"; + } + + /** + * The node the first-CMS-member rule picks, derived from cluster metadata rather than from the manager, so the + * test states the rule independently of the code under test. + */ + private static int firstCMSMember(Cluster cluster) + { + for (int n = 1; n <= cluster.size(); n++) + { + boolean isFirst = cluster.get(n).callOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + List members = new ArrayList<>(metadata.fullCMSMemberIds()); + members.sort(Comparator.comparingInt(NodeId::id)); + return !members.isEmpty() && members.get(0).equals(metadata.myNodeId()); + }); + + if (isFirst) + return n; + } + throw new AssertionError("no node considers itself the first CMS member"); + } + + /** Waits for {@code expected} to record a decision, then requires that no other node recorded one. */ + private static void awaitOnlyRecorder(Cluster cluster, String ks, int expected) + { + await("node " + expected + " records an auto-training decision") + .atMost(4, TimeUnit.MINUTES).pollInterval(5, TimeUnit.SECONDS) + .until(() -> decisionCount(cluster, ks, expected) > 0); + + for (int n = 1; n <= cluster.size(); n++) + { + if (n != expected) + Assert.assertEquals("only the first CMS member may record decisions, but node " + n + " did", + 0, decisionCount(cluster, ks, n)); + } + } + + private static int decisionCount(Cluster cluster, String ks, int node) + { + return cluster.coordinator(node).execute(LOCAL_DECISIONS, ConsistencyLevel.ONE, ks, TABLE).length; + } + + /** The id of the dictionary new writes on this node currently use (the manager's cached current dictionary). */ + private static long currentDictId(IInvokableInstance instance, String ks) + { + return instance.callOnInstance(() -> { + CompressionDictionary current = Keyspace.open(ks).getColumnFamilyStore(TABLE).compressionDictionaryManager().getCurrent(); + return current == null ? -1L : current.dictId().id; + }); + } + + /** The id of the latest dictionary persisted in {@code system_distributed.compression_dictionaries}. */ + private static long latestPersistedDictId(IInvokableInstance instance, String ks) + { + return instance.callOnInstance(() -> { + ColumnFamilyStore cfs = Keyspace.open(ks).getColumnFamilyStore(TABLE); + CompressionDictionary latest = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(ks, TABLE, cfs.metadata().id.toLongString()); + return latest == null ? -1L : latest.dictId().id; + }); + } + + /** The distinct dictionary ids actually stored in this node's live SSTables (-1 = no dictionary). */ + private static Set sstableDictIds(IInvokableInstance instance, String ks) + { + return instance.callOnInstance(() -> { + Set ids = new HashSet<>(); + ColumnFamilyStore cfs = Keyspace.open(ks).getColumnFamilyStore(TABLE); + try + { + // CompressionMetadata stores the dictionary the SSTable was compressed with, but exposes no public + // getter, so read the private field reflectively (its DictId is public). This is what proves which + // dictionary a given SSTable's data was actually compressed with. + Field field = CompressionMetadata.class.getDeclaredField("compressionDictionary"); + field.setAccessible(true); + for (SSTableReader sstable : cfs.getLiveSSTables()) + { + CompressionMetadata metadata = sstable.getCompressionMetadata(); + CompressionDictionary dictionary = metadata == null ? null : (CompressionDictionary) field.get(metadata); + ids.add(dictionary == null ? -1L : dictionary.dictId().id); + } + } + catch (ReflectiveOperationException e) + { + throw new RuntimeException(e); + } + return ids; + }); + } + + /** Writes {@code ROWS_PER_WINDOW} rows (at ALL) whose timestamps fall in {@code day}'s TWCS window. */ + private static void writeWindow(Cluster cluster, int day, String[] vocabulary) + { + long base = day * MICROS_PER_DAY; + Random random = new Random(day * 7919L); // deterministic per window + String insert = withKeyspace("INSERT INTO %s." + TABLE + " (id, v) VALUES (?, ?) USING TIMESTAMP ?"); + for (int i = 0; i < ROWS_PER_WINDOW; i++) + cluster.coordinator(1).execute(insert, ConsistencyLevel.ALL, day * 100_000 + i, row(vocabulary, random), base + i); + } + + /** A row value: {@code PHRASES_PER_ROW} space-joined phrases drawn (with repetition) from the window's vocabulary. */ + private static String row(String[] vocabulary, Random random) + { + StringBuilder sb = new StringBuilder(PHRASES_PER_ROW * (PHRASE_LEN + 1)); + for (int p = 0; p < PHRASES_PER_ROW; p++) + { + if (p > 0) + sb.append(' '); + sb.append(vocabulary[random.nextInt(vocabulary.length)]); + } + return sb.toString(); + } + + /** + * A deterministic vocabulary of {@code VOCAB_SIZE} distinct {@code PHRASE_LEN}-char phrases. Different seeds yield + * effectively disjoint vocabularies, so a dictionary trained on one window compresses the other window poorly - + * which is what makes the recency-trained candidate beat the stale one and get adopted. + */ + private static String[] vocabulary(long seed) + { + Random random = new Random(seed); + String[] vocabulary = new String[VOCAB_SIZE]; + for (int i = 0; i < VOCAB_SIZE; i++) + { + StringBuilder sb = new StringBuilder(PHRASE_LEN); + for (int c = 0; c < PHRASE_LEN; c++) + sb.append((char) ('a' + random.nextInt(26))); + vocabulary[i] = sb.toString(); + } + return vocabulary; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingVirtualTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingVirtualTableTest.java new file mode 100644 index 000000000000..42f71fc91265 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CompressionDictionaryAutoTrainingVirtualTableTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.db.compression.CompressionDictionary; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingHistory; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The auto-training history is node-local: each node records only the decisions it made. The + * {@code system_views_remote} counterpart reads that local table on every node, so one coordinator answers for the + * whole cluster, and a {@code node_id} restriction narrows the read to a single node. + */ +public class CompressionDictionaryAutoTrainingVirtualTableTest extends TestBaseImpl +{ + private static final String LOCAL = "SELECT keyspace_name, table_name, kind, improvement, promoted " + + "FROM system_views.compression_dictionary_auto_training"; + private static final String REMOTE = "SELECT node_id, keyspace_name, table_name, kind, improvement, promoted " + + "FROM system_views_remote.compression_dictionary_auto_training"; + + @Test + public void decisionsOfEveryNodeAreReadableFromOneCoordinator() throws Exception + { + try (Cluster cluster = init(Cluster.build(2).withConfig(c -> c.with(GOSSIP, NETWORK)).start())) + { + // a distinct decision per node, so each row identifies the node that recorded it + for (int i = 1; i <= 2; i++) + { + String keyspaceName = "ks" + i; + String tableName = "tbl" + i; + double improvement = 0.2 * i; + boolean promoted = i == 1; + cluster.get(i).runOnInstance(() -> + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, keyspaceName, tableName, + CompressionDictionary.Kind.ZSTD, + 0.5, 0.4, improvement, 0.15, promoted)); + } + + // the local table on each node sees only that node's own decision + assertThat(rowsOf(cluster.coordinator(1).execute(LOCAL, ConsistencyLevel.ONE))) + .describedAs("node1 local view") + .containsExactly("ks1|tbl1|ZSTD|0.2|true"); + assertThat(rowsOf(cluster.coordinator(2).execute(LOCAL, ConsistencyLevel.ONE))) + .describedAs("node2 local view") + .containsExactly("ks2|tbl2|ZSTD|0.4|false"); + + // one coordinator, both nodes' decisions + Object[][] all = cluster.coordinator(1).execute(REMOTE, ConsistencyLevel.ONE); + assertThat(all.length).describedAs("one row per node").isEqualTo(2); + assertThat(rowsOf(all, 1)) + .describedAs("cluster-wide view from node1") + .containsExactlyInAnyOrder("ks1|tbl1|ZSTD|0.2|true", "ks2|tbl2|ZSTD|0.4|false"); + + // restricting node_id reads that node alone, and node_id identifies the recording node + for (Object[] row : all) + { + int nodeId = (Integer) row[0]; + Object[][] single = cluster.coordinator(1).execute(REMOTE + " WHERE node_id = ?", + ConsistencyLevel.ONE, nodeId); + assertThat(rowsOf(single, 1)) + .describedAs("node_id " + nodeId + " in isolation") + .containsExactly(rowOf(row, 1)); + } + } + } + + private static List rowsOf(Object[][] rows) + { + return rowsOf(rows, 0); + } + + private static List rowsOf(Object[][] rows, int from) + { + List result = new ArrayList<>(); + for (Object[] row : rows) + result.add(rowOf(row, from)); + return result; + } + + private static String rowOf(Object[] row, int from) + { + StringBuilder sb = new StringBuilder(); + for (int i = from; i < row.length; i++) + { + if (i > from) + sb.append('|'); + sb.append(row[i]); + } + return sb.toString(); + } +} diff --git a/test/unit/org/apache/cassandra/ServerTestUtils.java b/test/unit/org/apache/cassandra/ServerTestUtils.java index ea6c53003c3c..83ba36cdfba5 100644 --- a/test/unit/org/apache/cassandra/ServerTestUtils.java +++ b/test/unit/org/apache/cassandra/ServerTestUtils.java @@ -40,6 +40,7 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -195,6 +196,7 @@ public void uncaughtException(Thread t, Throwable e) initCMS(); SystemKeyspace.persistLocalMetadata(); AuditLogManager.instance.initialize(); + CompressionDictionaryAutoTrainingManager.instance.start(); DiskErrorsHandlerService.configure(); diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingAlterTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingAlterTest.java new file mode 100644 index 000000000000..cac8221b022b --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingAlterTest.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.concurrent.Refs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * ALTER TABLE must be able to turn compression dictionary auto-training on and off without a node restart. + */ +public class CompressionDictionaryAutoTrainingAlterTest extends CQLTester +{ + private static final String DICT_AUTO = "{'class':'ZstdDictionaryCompressor','auto_training_enabled':'true'}"; + private static final String DICT_PLAIN = "{'class':'ZstdDictionaryCompressor'}"; + private static final String TWCS = "{'class':'TimeWindowCompactionStrategy'}"; + + @Test + public void alterEnablesAndDisablesAutoTraining() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_PLAIN + " AND compaction = " + TWCS); + assertAutoTrainingEnabled(false); + + alterTable("ALTER TABLE %s WITH compression = " + DICT_AUTO); + assertAutoTrainingEnabled(true); + + alterTable("ALTER TABLE %s WITH compression = " + DICT_PLAIN); + assertAutoTrainingEnabled(false); + } + + @Test + public void autoTrainingEnabledAtCreateIsVisible() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + assertAutoTrainingEnabled(true); + } + + /** + * Disabling dictionary compression tears the scheduler down; re-enabling must leave the table able to train + * again. A scheduler that owned and shut down a training executor here would reject every later submit, + * permanently disabling training for the table. + */ + @Test + public void disableThenReEnableLeavesTrainingUsable() throws Throwable + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + assertAutoTrainingEnabled(true); + + execute("INSERT INTO %s (id, v) VALUES (?, ?)", 1, "v1"); + flush(); + + alterTable("ALTER TABLE %s WITH compression = {'class':'LZ4Compressor'}"); + assertThat(manager().isEnabled()) + .describedAs("dictionary compression disabled") + .isFalse(); + + alterTable("ALTER TABLE %s WITH compression = " + DICT_AUTO); + assertThat(manager().isEnabled()) + .describedAs("dictionary compression re-enabled") + .isTrue(); + assertAutoTrainingEnabled(true); + assertThat(manager().isTrainingRunning()).isFalse(); + + // Training may still fail for legitimate reasons here (too few samples, for instance); what must not + // happen is the submit being rejected because the executor was torn down by the disable. + try + { + manager().train(false, Map.of()); + } + catch (Throwable t) + { + assertThat(t) + .describedAs("training must not be rejected by a shut-down executor after a disable/enable cycle") + .isNotInstanceOf(RejectedExecutionException.class); + assertThat(t.getCause()) + .describedAs("training must not be rejected by a shut-down executor after a disable/enable cycle") + .isNotInstanceOf(RejectedExecutionException.class); + } + } + + /** + * Turning auto-training off with ALTER must exclude the table from the check cycle: checkTable() reads the + * flag from the live schema and returns before scheduling any training. + */ + @Test + public void alterDisablingAutoTrainingExcludesTableFromChecks() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); + + AtomicInteger scheduled = new AtomicInteger(); + // Everything checkTable() consults after the flag is stubbed out, so the flag is the only thing that can + // stop it: a non-null latest dictionary to compare against, and a fragment to train on. + CompressionDictionaryAutoTrainingManager autoTrainer = new CompressionDictionaryAutoTrainingManager() + { + @Override + ColumnFamilyStore.RefViewFragment resolveViewFragment(ColumnFamilyStore c) + { + return new ColumnFamilyStore.RefViewFragment(Collections.emptyList(), + Collections.emptyList(), + Refs.tryRef(Collections.emptyList())); + } + + @Override + CompressionDictionary retrieveLatestDictionary(ColumnFamilyStore c) + { + CompressionDictionary latest = mock(CompressionDictionary.class); + when(latest.kind()).thenReturn(CompressionDictionary.Kind.ZSTD); + return latest; + } + + @Override + CompressionDictionary scheduleTraining(ColumnFamilyStore c, + ColumnFamilyStore.RefViewFragment refViewFragment, + CompressionDictionaryManager m) + { + scheduled.incrementAndGet(); + refViewFragment.close(); + return null; + } + }; + + autoTrainer.checkTable(cfs); + assertThat(scheduled.get()).describedAs("auto training enabled, table is trained").isEqualTo(1); + + alterTable("ALTER TABLE %s WITH compression = " + DICT_PLAIN); + assertAutoTrainingEnabled(false); + + scheduled.set(0); + autoTrainer.checkTable(cfs); + assertThat(scheduled.get()).describedAs("auto training disabled, table is excluded").isZero(); + } + + private CompressionDictionaryManager manager() + { + return getCurrentColumnFamilyStore().compressionDictionaryManager(); + } + + /** + * The MBean getter and the training config must not disagree; they read the same option. + */ + private void assertAutoTrainingEnabled(boolean expected) + { + CompressionDictionaryManager manager = manager(); + + assertThat(manager.isAutoTrainingEnabled()) + .describedAs("CompressionDictionaryManager.isAutoTrainingEnabled()") + .isEqualTo(expected); + + assertThat(manager.createTrainingConfig(Map.of()).autoTrainingEnabled) + .describedAs("CompressionDictionaryTrainingConfig.autoTrainingEnabled") + .isEqualTo(expected); + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingCompactionValidationTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingCompactionValidationTest.java new file mode 100644 index 000000000000..b1b9e41abdf7 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingCompactionValidationTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Verifies the schema-level guard that compression dictionary auto-training ({@code auto_training_enabled = true}) + * is only permitted on tables using {@code TimeWindowCompactionStrategy}: both CREATE TABLE and ALTER TABLE accept + * it on a TWCS table and reject it on any other compaction strategy. + */ +public class CompressionDictionaryAutoTrainingCompactionValidationTest extends CQLTester +{ + private static final String DICT_AUTO = "{'class':'ZstdDictionaryCompressor','auto_training_enabled':'true'}"; + private static final String DICT_PLAIN = "{'class':'ZstdDictionaryCompressor'}"; + private static final String TWCS = "{'class':'TimeWindowCompactionStrategy'}"; + private static final String STCS = "{'class':'SizeTieredCompactionStrategy'}"; + private static final String EXPECTED = "only supported on tables using TimeWindowCompactionStrategy"; + + @Test + public void createAllowsAutoTrainingOnTwcs() + { + // must not throw + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + } + + @Test + public void createRejectsAutoTrainingOnNonTwcs() + { + assertThatThrownBy(() -> + createTableMayThrow("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + STCS)) + .hasMessageContaining(EXPECTED); + } + + @Test + public void alterAllowsEnablingAutoTrainingOnTwcs() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_PLAIN + " AND compaction = " + TWCS); + // enabling auto-training on an existing TWCS table must not throw + alterTable("ALTER TABLE %s WITH compression = " + DICT_AUTO); + } + + @Test + public void alterRejectsEnablingAutoTrainingOnNonTwcs() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_PLAIN + " AND compaction = " + STCS); + assertThatThrownBy(() -> + alterTableMayThrow("ALTER TABLE %s WITH compression = " + DICT_AUTO)) + .hasMessageContaining(EXPECTED); + } + + @Test + public void createLikeAllowsAutoTrainingInheritedOnTwcs() + { + String source = createTable(KEYSPACE, "CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + createTableLike("CREATE TABLE %s LIKE %s", source, KEYSPACE, KEYSPACE); + } + + /** + * Compression carrying auto-training is inherited from the source while the WITH clause overrides compaction, + * so neither the source params nor the WITH clause alone reveals the unsupported combination. + */ + @Test + public void createLikeRejectsInheritedAutoTrainingWhenCompactionOverriddenToNonTwcs() + { + String source = createTable(KEYSPACE, "CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + assertThatThrownBy(() -> + createTableLike("CREATE TABLE %s LIKE %s WITH compaction = " + STCS, source, KEYSPACE, KEYSPACE)) + .hasStackTraceContaining(EXPECTED); + } + + /** + * The mirror image: compaction is inherited and non-TWCS, auto-training arrives via the WITH clause. + */ + @Test + public void createLikeRejectsAutoTrainingAddedOnInheritedNonTwcs() + { + String source = createTable(KEYSPACE, "CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_PLAIN + " AND compaction = " + STCS); + assertThatThrownBy(() -> + createTableLike("CREATE TABLE %s LIKE %s WITH compression = " + DICT_AUTO, source, KEYSPACE, KEYSPACE)) + .hasStackTraceContaining(EXPECTED); + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingConcurrencyTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingConcurrencyTest.java new file mode 100644 index 000000000000..01b576de7079 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingConcurrencyTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.concurrent.Refs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Verifies the manual-vs-automatic training mutual exclusion: a user's training always has priority, so if a + * training is already in progress when the auto-training cycle reaches a table it must back off and not train. + *

+ * This uses a real dictionary-compressed table (so {@code checkTable()} runs its real logic) but simulates the + * "a training is already running" state rather than running a real training. + */ +public class CompressionDictionaryAutoTrainingConcurrencyTest extends CQLTester +{ + @Test + public void autoTrainingBacksOffWhenATrainingIsAlreadyInProgress() + { + String table = createTable("CREATE TABLE %s (id int PRIMARY KEY, data text) WITH compression = " + + "{'class':'ZstdDictionaryCompressor', 'auto_training_enabled':'true'} " + + "AND compaction = {'class':'TimeWindowCompactionStrategy'}"); + ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(table); + + // Stand-in for the table's manager whose training state we drive; it plays the role of a manager on + // which a user has (or has not) invoked training. + CompressionDictionaryManager manager = mock(CompressionDictionaryManager.class); + when(manager.isEnabled()).thenReturn(true); + when(manager.isAutoTrainingEnabled()).thenReturn(true); + + // A non-null "latest" dictionary so that, absent a back-off, checkTable() would proceed to training + // (i.e. the only thing that can stop it here is the in-progress-training guard). + CompressionDictionary latest = mock(CompressionDictionary.class); + when(latest.kind()).thenReturn(CompressionDictionary.Kind.ZSTD); + // The recency-bias resolver currently returns null (auto-training paused), so inject a non-null view + // fragment here; that keeps the in-progress-training guard as the only thing that can stop checkTable(). + // A real fragment rather than a mock: checkTable() re-references its SSTables, and a Mockito mock + // bypasses the constructor and so leaves the final sstables field null, which cannot happen in production. + ColumnFamilyStore.RefViewFragment refViewFragment = + new ColumnFamilyStore.RefViewFragment(Collections.emptyList(), + Collections.emptyList(), + Refs.tryRef(Collections.emptyList())); + + AtomicInteger scheduleTrainingCalls = new AtomicInteger(); + CompressionDictionaryAutoTrainingManager autoTrainer = new CompressionDictionaryAutoTrainingManager() + { + @Override + ColumnFamilyStore.RefViewFragment resolveViewFragment(ColumnFamilyStore c) + { + return refViewFragment; + } + + @Override + CompressionDictionaryManager getCompressionDictionaryManager(ColumnFamilyStore c) + { + return manager; + } + + @Override + CompressionDictionary retrieveLatestDictionary(ColumnFamilyStore c) + { + return latest; + } + + @Override + CompressionDictionary scheduleTraining(ColumnFamilyStore c, + ColumnFamilyStore.RefViewFragment fragment, + CompressionDictionaryManager m) + { + scheduleTrainingCalls.incrementAndGet(); + return null; + } + }; + + try (autoTrainer) + { + // control: no training in progress -> auto-training proceeds all the way to scheduling a training + when(manager.isTrainingRunning()).thenReturn(false); + autoTrainer.checkTable(cfs); + assertThat(scheduleTrainingCalls.get()) + .as("with no training in progress, auto-training should proceed to training") + .isEqualTo(1); + + // the assertion under test: a training is already running (user has priority) -> auto-training backs off + scheduleTrainingCalls.set(0); + when(manager.isTrainingRunning()).thenReturn(true); + autoTrainer.checkTable(cfs); + assertThat(scheduleTrainingCalls.get()) + .as("a training is already in progress -> auto-training must back off and NOT train") + .isZero(); + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingDictionaryKindTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingDictionaryKindTest.java new file mode 100644 index 000000000000..abe1f64bd53f --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingDictionaryKindTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.concurrent.Refs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * The adoption decision compares a candidate against the latest stored dictionary by running both through a + * trainer built from the table's current compression params. That is only meaningful if the current compressor + * can consume the latest dictionary: the ratio computation feeds it raw bytes, and it accepts arbitrary bytes + * as a raw content dictionary, so a dictionary of a foreign kind yields a meaningless baseline rather than an + * error. Since the latest dictionary is the newest dict_id for the table regardless of kind, a table whose + * dictionary compressor kind changed can see one. Such a round must be skipped. + */ +public class CompressionDictionaryAutoTrainingDictionaryKindTest extends CQLTester +{ + private static final String DICT_AUTO = "{'class':'ZstdDictionaryCompressor','auto_training_enabled':'true'}"; + private static final String TWCS = "{'class':'TimeWindowCompactionStrategy'}"; + + @Test + public void zstdCompressorConsumesZstdDictionary() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + + assertThat(CompressionDictionaryAutoTrainingManager.canConsume(getCurrentColumnFamilyStore(), + dictionaryOfKind(CompressionDictionary.Kind.ZSTD))) + .isTrue(); + } + + @Test + public void nonDictionaryCompressorConsumesNothing() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = {'class':'LZ4Compressor'} AND compaction = " + TWCS); + + assertThat(CompressionDictionaryAutoTrainingManager.canConsume(getCurrentColumnFamilyStore(), + dictionaryOfKind(CompressionDictionary.Kind.ZSTD))) + .isFalse(); + } + + @Test + public void unconsumableLatestDictionarySkipsRound() + { + assertThat(trainedWithLatest(unconsumableDictionary())) + .describedAs("latest dictionary of a kind the compressor cannot consume, nothing comparable to evaluate") + .isZero(); + } + + @Test + public void consumableLatestDictionaryProceeds() + { + assertThat(trainedWithLatest(dictionaryOfKind(CompressionDictionary.Kind.ZSTD))) + .describedAs("latest dictionary the compressor can consume, round proceeds") + .isEqualTo(1); + } + + /** + * Runs a check cycle over a real dictionary-compressed table with {@code latest} as the stored dictionary, + * and returns how many times training was scheduled. Everything checkTable() consults after the kind guard is + * stubbed out, so the guard is the only thing that can stop it. + */ + private int trainedWithLatest(CompressionDictionary latest) + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) " + + "WITH compression = " + DICT_AUTO + " AND compaction = " + TWCS); + ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); + + AtomicInteger scheduled = new AtomicInteger(); + CompressionDictionaryAutoTrainingManager autoTrainer = new CompressionDictionaryAutoTrainingManager() + { + @Override + CompressionDictionary retrieveLatestDictionary(ColumnFamilyStore c) + { + return latest; + } + + @Override + ColumnFamilyStore.RefViewFragment resolveViewFragment(ColumnFamilyStore c) + { + return new ColumnFamilyStore.RefViewFragment(Collections.emptyList(), + Collections.emptyList(), + Refs.tryRef(Collections.emptyList())); + } + + @Override + CompressionDictionary scheduleTraining(ColumnFamilyStore c, + ColumnFamilyStore.RefViewFragment refViewFragment, + CompressionDictionaryManager m) + { + scheduled.incrementAndGet(); + refViewFragment.close(); + return null; + } + }; + + autoTrainer.checkTable(cfs); + return scheduled.get(); + } + + private static CompressionDictionary dictionaryOfKind(CompressionDictionary.Kind kind) + { + CompressionDictionary dictionary = mock(CompressionDictionary.class); + when(dictionary.kind()).thenReturn(kind); + return dictionary; + } + + /** + * Kind has a single constant today, so a dictionary of a foreign kind cannot be built; a dictionary reporting + * no kind stands in for one, since it drives canConsumeDictionary() down the same rejecting branch. + */ + private static CompressionDictionary unconsumableDictionary() + { + return dictionaryOfKind(null); + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingEvaluationTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingEvaluationTest.java new file mode 100644 index 000000000000..c325bd6b9150 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingEvaluationTest.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager.CompressionRatioEvaluator; + +import static org.apache.cassandra.io.sstable.metadata.MetadataCollector.NO_COMPRESSION_RATIO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests the adoption decision in {@link CompressionDictionaryAutoTrainingManager#newDictionaryBetterThanLatest}: + * the freshly-trained candidate is promoted only if it improves the compression ratio over the current + * dictionary by at least the configured threshold, where {@code improvement = (latest - candidate) / latest} + * (ratios are compressed/uncompressed, so lower is better). + *

+ * Both dictionaries and the held-out evaluation are mocked (via the {@code createTrainer}/{@code createEvaluator} + * seams), so this exercises purely the improvement math and threshold comparison - no sampling, training or + * real compression happens. + */ +public class CompressionDictionaryAutoTrainingEvaluationTest +{ + private static final float THRESHOLD = 0.15f; + + @BeforeClass + public static void setUpClass() + { + // needed so ColumnFamilyStore can be class-loaded for mocking; creates no keyspace/table + DatabaseDescriptor.daemonInitialization(); + } + + @Test + public void candidateThatDoesNotBeatLatestEnoughIsNotPromoted() + { + // latest=0.50, candidate=0.49 -> improvement = 0.02, below the 0.15 threshold + assertThat(isPromoted(0.50, 0.49)).isFalse(); + } + + @Test + public void candidateThatBeatsLatestByThresholdIsPromoted() + { + // latest=0.50, candidate=0.30 -> improvement = 0.40, at/above the 0.15 threshold + assertThat(isPromoted(0.50, 0.30)).isTrue(); + } + + /** + * computeSampleCompressionRatio returns NO_COMPRESSION_RATIO (-1.0) when nothing was sampled, and -1.0 is + * finite, so it reaches the improvement arithmetic. Unguarded it inverts the comparison rather than failing: + * the relative improvement formula turns a -1.0 on either side into a large positive number. + */ + @Test + public void unmeasurableBaselineDoesNotPromote() + { + // (-1.0 - 0.30) / -1.0 = +1.30 -> would clear any threshold on a baseline that measured nothing + assertThat(isPromoted(NO_COMPRESSION_RATIO, 0.30)).isFalse(); + } + + @Test + public void unmeasurableCandidateDoesNotPromote() + { + // (0.50 - -1.0) / 0.50 = +3.00 -> the worst case: a candidate that measured NOTHING wins by the + // largest possible margin + assertThat(isPromoted(0.50, NO_COMPRESSION_RATIO)).isFalse(); + } + + /** + * These two are safe even without the guard - (-1.0 + 1.0) / -1.0 is -0.0, and a zero baseline gives + * -Infinity - so they are here to keep that accidental safety pinned down rather than relied upon. + */ + @Test + public void bothUnmeasurableDoesNotPromote() + { + assertThat(isPromoted(NO_COMPRESSION_RATIO, NO_COMPRESSION_RATIO)).isFalse(); + } + + @Test + public void zeroBaselineDoesNotPromote() + { + assertThat(isPromoted(0.0, 0.30)).isFalse(); + } + + /** + * Invokes the real {@code newDictionaryBetterThanLatest} with the held-out ratios for the current (latest) + * and freshly-trained (candidate) dictionaries mocked to the given values. + */ + private boolean isPromoted(double latestRatio, double candidateRatio) + { + ColumnFamilyStore cfs = mock(ColumnFamilyStore.class); + when(cfs.getKeyspaceName()).thenReturn("ks"); + when(cfs.getTableName()).thenReturn("tbl"); + + CompressionDictionary latest = mock(CompressionDictionary.class); + CompressionDictionary candidate = mock(CompressionDictionary.class); + + ICompressionDictionaryTrainer trainer = mock(ICompressionDictionaryTrainer.class); + ColumnFamilyStore.RefViewFragment refViewFragment = mock(ColumnFamilyStore.RefViewFragment.class); + + // the evaluation itself is mocked: it returns a fixed held-out ratio per dictionary + CompressionRatioEvaluator evaluator = mock(CompressionRatioEvaluator.class); + when(evaluator.evaluate(latest)).thenReturn(latestRatio); + when(evaluator.evaluate(candidate)).thenReturn(candidateRatio); + + CompressionDictionaryTrainingConfig config = CompressionDictionaryTrainingConfig.builder() + .autoTrainingImprovementThreshold(THRESHOLD) + .build(); + + CompressionDictionaryAutoTrainingManager autoTrainer = new CompressionDictionaryAutoTrainingManager() + { + @Override + ICompressionDictionaryTrainer createTrainer(ColumnFamilyStore c) + { + return trainer; + } + + @Override + CompressionRatioEvaluator createEvaluator(ColumnFamilyStore c, + ColumnFamilyStore.RefViewFragment r, + ICompressionDictionaryTrainer t, + CompressionDictionaryTrainingConfig cfg) + { + return evaluator; + } + }; + + return autoTrainer.newDictionaryBetterThanLatest(cfs, latest, candidate, refViewFragment, config); + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManagerLifecycleTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManagerLifecycleTest.java new file mode 100644 index 000000000000..e63c0121685e --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingManagerLifecycleTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.schema.TableMetadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Scheduling-skeleton tests for {@link CompressionDictionaryAutoTrainingManager}. + *

+ * They exercise the real {@code scheduleWithFixedDelay} loop through the {@code @VisibleForTesting} + * seams ({@code start(delay, interval, unit)}, {@code checkAllTables()}, {@code getTables()}, + * {@code isFirstCMSMember()}, {@code checkTable()}) and deliberately do NOT touch real tables, CMS + * membership, sampling, training or dictionary comparison. + */ +public class CompressionDictionaryAutoTrainingManagerLifecycleTest +{ + private static final String USER_KS = "auto_training_test_ks"; + + @BeforeClass + public static void setUpClass() + { + DatabaseDescriptor.daemonInitialization(); + } + + @Test + public void schedulerLoopsWhileStartedAndStopsOnClose() + { + AtomicInteger invocations = new AtomicInteger(); + + CompressionDictionaryAutoTrainingManager manager = new CompressionDictionaryAutoTrainingManager() + { + @Override + void checkAllTables() + { + invocations.incrementAndGet(); + } + }; + + int intervalSeconds = 5; + int countAtClose; + try + { + manager.start(intervalSeconds, intervalSeconds, TimeUnit.SECONDS); + + // The scheduled check must fire repeatedly (>= 3 times) on its fixed schedule, not just once. + await().atMost(35, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(invocations.get()) + .as("checkAllTables() should be invoked repeatedly on the %ds schedule", intervalSeconds) + .isGreaterThanOrEqualTo(3)); + } + finally + { + countAtClose = invocations.get(); + manager.close(); + } + + // After close() the scheduled task is cancelled, so the counter must stop advancing: assert it holds + // (bar a single already-in-flight tick) for more than two intervals. + await("no further invocations after close()") + .during(2L * intervalSeconds, TimeUnit.SECONDS) + .atMost(2L * intervalSeconds + 5, TimeUnit.SECONDS) + .until(() -> invocations.get() <= countAtClose + 1); + } + + /** + * Drives the real {@link CompressionDictionaryAutoTrainingManager#checkAllTables()} via the scheduler + * (never called directly, never overridden) and steers it only through the {@code getTables()} and + * {@code isFirstCMSMember()} seams to walk its branches: + *

    + *
  1. CMS leader, no tables - nothing is checked;
  2. + *
  3. CMS leader, tables that are all ineligible — one per {@code isEligibleTable()} branch, so every + * branch is exercised and none reach {@code checkTable()};
  4. + *
  5. not the CMS leader - the loop keeps firing but bails before touching any table.
  6. + *
+ */ + @Test + public void checkAllTablesHonoursCmsLeadershipAndTableEligibility() + { + ControllableAutoTrainingManager manager = new ControllableAutoTrainingManager(); + int intervalSeconds = 2; + try + { + manager.start(intervalSeconds, intervalSeconds, TimeUnit.SECONDS); + + // ---- case 1: this node IS the CMS leader, but there are no tables to check ---- + manager.cmsLeader = true; + manager.tables = Collections.emptyList(); + awaitAtLeast(manager.cmsMemberChecks, manager.cmsMemberChecks.get() + 2); + assertThat(manager.tableChecks.get()) + .as("no tables -> checkTable() must never run") + .isZero(); + + // ---- case 2: CMS leader, but every table is ineligible - one per isEligibleTable() branch ---- + ColumnFamilyStore systemKs = mock(ColumnFamilyStore.class); + when(systemKs.getKeyspaceName()).thenReturn("system"); + + ColumnFamilyStore index = mock(ColumnFamilyStore.class); + when(index.getKeyspaceName()).thenReturn(USER_KS); + when(index.isIndex()).thenReturn(true); + + ColumnFamilyStore view = mock(ColumnFamilyStore.class); + TableMetadata viewMeta = mock(TableMetadata.class); + when(view.getKeyspaceName()).thenReturn(USER_KS); + when(view.metadata()).thenReturn(viewMeta); + when(viewMeta.isView()).thenReturn(true); + + ColumnFamilyStore staticCompact = mock(ColumnFamilyStore.class); + TableMetadata staticMeta = mock(TableMetadata.class); + when(staticCompact.getKeyspaceName()).thenReturn(USER_KS); + when(staticCompact.metadata()).thenReturn(staticMeta); + when(staticMeta.isStaticCompactTable()).thenReturn(true); + + manager.tables = Arrays.asList(systemKs, index, view, staticCompact); + awaitAtLeast(manager.cmsMemberChecks, manager.cmsMemberChecks.get() + 2); + + assertThat(manager.tableChecks.get()) + .as("every table is ineligible -> checkTable() must never run") + .isZero(); + + // every isEligibleTable() branch was actually reached/evaluated + verify(systemKs, atLeastOnce()).getKeyspaceName(); + verify(index, atLeastOnce()).isIndex(); + verify(viewMeta, atLeastOnce()).isView(); + verify(staticMeta, atLeastOnce()).isStaticCompactTable(); + + // ---- case 3: NOT the CMS leader -> checkAllTables() bails before iterating tables, every cycle ---- + manager.cmsLeader = false; + manager.tableChecks.set(0); + awaitAtLeast(manager.cmsMemberChecks, manager.cmsMemberChecks.get() + 3); + assertThat(manager.tableChecks.get()) + .as("not CMS leader -> tables are never iterated") + .isZero(); + } + finally + { + manager.close(); + } + } + + /** + * Once {@link CompressionDictionaryAutoTrainingManager#close()} is called the scheduled check is + * cancelled (and further guarded by the {@code closed} flag), so no auto-training work may happen + * afterwards: {@code isFirstCMSMember()} must never be consulted again, no matter how much time passes. + */ + @Test + public void noChecksHappenAfterClose() + { + ControllableAutoTrainingManager manager = new ControllableAutoTrainingManager(); + int intervalSeconds = 2; + + manager.start(intervalSeconds, intervalSeconds, TimeUnit.SECONDS); + // make sure it is genuinely running and consulting CMS membership before we close it + awaitAtLeast(manager.cmsMemberChecks, 3); + + manager.close(); + int checksAtClose = manager.cmsMemberChecks.get(); + + // over several further intervals the count must stay frozen - i.e. the loop really stopped + await("isFirstCMSMember() must not be invoked after close()") + .during(3L * intervalSeconds, TimeUnit.SECONDS) + .atMost(3L * intervalSeconds + 5, TimeUnit.SECONDS) + .until(() -> manager.cmsMemberChecks.get() == checksAtClose); + } + + /** + * Waits (bounded) until {@code counter} reaches {@code target}, i.e. the scheduled check has fired + * enough times. + */ + private static void awaitAtLeast(AtomicInteger counter, int target) + { + await().atMost(30, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(counter.get()) + .as("scheduled check should reach at least %d invocations", target) + .isGreaterThanOrEqualTo(target)); + } + + /** + * A manager whose CMS-leadership answer and table set are test-controlled, and which records how often + * the loop consults CMS membership and how often a table reached {@code checkTable()}. {@code checkTable()} + * is stubbed to a counter so no real per-table work (training, comparison) ever runs. + */ + private static class ControllableAutoTrainingManager extends CompressionDictionaryAutoTrainingManager + { + volatile boolean cmsLeader = true; + volatile Iterable tables = Collections.emptyList(); + final AtomicInteger cmsMemberChecks = new AtomicInteger(); + final AtomicInteger tableChecks = new AtomicInteger(); + + @Override + boolean isFirstCMSMember() + { + cmsMemberChecks.incrementAndGet(); + return cmsLeader; + } + + @Override + Iterable getTables() + { + return tables; + } + + @Override + void checkTable(ColumnFamilyStore cfs) + { + tableChecks.incrementAndGet(); + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyBiasViewFragmentResolverTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyBiasViewFragmentResolverTest.java new file mode 100644 index 000000000000..bf5c78552326 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyBiasViewFragmentResolverTest.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Isolated test of {@link RecencyBiasViewFragmentResolver}: it only exercises which SSTables the resolver selects, + * with no training involved. The resolver walks TWCS windows newest-first, adding each window whole, until it has + * gathered {@code acceptableTotalSampleSize} uncompressed bytes or has reached back {@code auto_training_twcs_max_windows} + * windows. It resolves to {@code null} (and the auto-trainer backs off) when the table is not on TWCS, has no + * SSTables, or the windows within the cap do not hold enough data to reach the sample floor. + */ +public class CompressionDictionaryAutoTrainingRecencyBiasViewFragmentResolverTest extends CQLTester +{ + private static final long MICROS_PER_DAY = 86_400L * 1_000_000L; + private static final String TWO_KIB_VALUE = "x".repeat(2048); + private static final int ROWS_PER_WINDOW = 50; + private static final long UNREACHABLE_TARGET = 100L << 20; // 100 MiB, far more than the ~400 KiB of test data + + @Test + public void newestWindowAloneCanMeetTheTarget() throws Throwable + { + ColumnFamilyStore cfs = fourEqualDayWindows(); + long newest = maxTimestampOfDay(4); + + // a tiny target is satisfied by the newest window alone, and the default cap of 1 stops it right there + try (ColumnFamilyStore.RefViewFragment fragment = + new RecencyBiasViewFragmentResolver(cfs, config(8, 1)).resolveViewFragment()) + { + assertThat(fragment).isNotNull(); + assertThat(fragment.sstables) + .as("newest window already meets the target under a 1-window cap -> just the newest window") + .hasSize(1) + .allMatch(sstable -> sstable.getMaxTimestamp() == newest); + } + } + + @Test + public void keepsAddingWholeWindowsUntilTheTargetIsMet() throws Throwable + { + ColumnFamilyStore cfs = fourEqualDayWindows(); + long[] cumulative = cumulativeUncompressedNewestFirst(cfs); + + // target that one window does not satisfy but two do (midpoint of the 1- and 2-window totals) + CompressionDictionaryTrainingConfig config = configTargeting(midpoint(cumulative[0], cumulative[1])); + assertThat((long) config.acceptableTotalSampleSize).isGreaterThan(cumulative[0]).isLessThanOrEqualTo(cumulative[1]); + + try (ColumnFamilyStore.RefViewFragment fragment = new RecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment()) + { + assertThat(fragment.sstables) + .as("one window short, two enough -> exactly the two newest windows") + .hasSize(2) + .allMatch(sstable -> sstable.getMaxTimestamp() >= 3 * MICROS_PER_DAY); + } + } + + @Test + public void reachesBackAcrossThreeWindowsWhenNeeded() throws Throwable + { + ColumnFamilyStore cfs = fourEqualDayWindows(); + long[] cumulative = cumulativeUncompressedNewestFirst(cfs); + + // target that two windows do not satisfy but three do (midpoint of the 2- and 3-window totals) + CompressionDictionaryTrainingConfig config = configTargeting(midpoint(cumulative[1], cumulative[2])); + assertThat((long) config.acceptableTotalSampleSize).isGreaterThan(cumulative[1]).isLessThanOrEqualTo(cumulative[2]); + + try (ColumnFamilyStore.RefViewFragment fragment = new RecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment()) + { + assertThat(fragment.sstables) + .as("two windows short, three enough -> exactly the three newest windows") + .hasSize(3) + .allMatch(sstable -> sstable.getMaxTimestamp() >= 2 * MICROS_PER_DAY); + } + } + + @Test + public void backsOffWhenTheWindowCapCannotReachEnoughData() throws Throwable + { + ColumnFamilyStore cfs = fourEqualDayWindows(); + long[] cumulative = cumulativeUncompressedNewestFirst(cfs); + + // this target needs three windows, but the cap allows only two -> not enough -> back off + long needsThreeWindows = midpoint(cumulative[1], cumulative[2]); + int maxTotalSampleSize = (int) Math.max(10, Math.ceil(needsThreeWindows * 10.0 / 8.0)); + CompressionDictionaryTrainingConfig config = CompressionDictionaryTrainingConfig.builder() + .maxTotalSampleSize(maxTotalSampleSize) + .autoTrainingTwcsMaxWindows(2) + .build(); + + assertThat(new RecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment()) + .as("two windows do not hold enough and the cap forbids a third -> back off (null)") + .isNull(); + } + + @Test + public void backsOffWhenNoRecentWindowsHoldEnough() throws Throwable + { + ColumnFamilyStore cfs = fourEqualDayWindows(); + + // even reaching back across all windows (generous cap) cannot meet an unreachable target -> back off + assertThat(new RecencyBiasViewFragmentResolver(cfs, config(UNREACHABLE_TARGET, 10)).resolveViewFragment()) + .as("not enough data anywhere in reach -> back off (null)") + .isNull(); + } + + @Test + public void skipsEmptyWindowsWithoutSpendingTheCap() throws Throwable + { + // three windows with wide gaps between them (days 2-4 and 6-9 hold no SSTables) + ColumnFamilyStore cfs = twcsDayWindowTable(); + writeDayWindow(1); + writeDayWindow(5); + writeDayWindow(10); + assertThat(cfs.getLiveSSTables()).hasSize(3); + + long[] cumulative = cumulativeUncompressedNewestFirst(cfs); // newest-first: day 10, day 5, day 1 + + // need two windows' worth of data, cap of two windows: the gap windows between day 10 and day 5 must not + // count against the cap, otherwise it would stop at day 10 alone and fall short of the target + CompressionDictionaryTrainingConfig config = config(midpoint(cumulative[0], cumulative[1]), 2); + assertThat((long) config.acceptableTotalSampleSize).isGreaterThan(cumulative[0]).isLessThanOrEqualTo(cumulative[1]); + + try (ColumnFamilyStore.RefViewFragment fragment = new RecencyBiasViewFragmentResolver(cfs, config).resolveViewFragment()) + { + assertThat(fragment.sstables) + .as("empty windows are skipped for free -> the two newest data-bearing windows (days 10 and 5)") + .hasSize(2) + .allMatch(sstable -> sstable.getMaxTimestamp() >= 5 * MICROS_PER_DAY); + } + } + + @Test + public void resolvesNothingForNonTwcs() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) WITH compaction = " + + "{'class':'SizeTieredCompactionStrategy'}"); + ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); + + assertThat(new RecencyBiasViewFragmentResolver(cfs, config(8, 5)).resolveViewFragment()) + .as("recency-bias resolution is only defined for TWCS; anything else resolves to null") + .isNull(); + } + + @Test + public void resolvesNothingWhenTwcsHasNoSSTables() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) WITH compaction = " + + "{'class':'TimeWindowCompactionStrategy'}"); + ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); + + assertThat(new RecencyBiasViewFragmentResolver(cfs, config(8, 5)).resolveViewFragment()) + .as("a TWCS table with no SSTables has nothing to train on and must resolve to null") + .isNull(); + } + + /** + * Builds a training config whose {@code acceptableTotalSampleSize} floor is approximately the requested number of + * bytes (inverting {@code acceptable = maxTotalSampleSize / 10 * 8}), with the given TWCS window cap. + */ + private static CompressionDictionaryTrainingConfig config(long desiredAcceptableBytes, int maxWindows) + { + int maxTotalSampleSize = (int) Math.max(10, Math.ceil(desiredAcceptableBytes * 10.0 / 8.0)); + return CompressionDictionaryTrainingConfig.builder() + .maxTotalSampleSize(maxTotalSampleSize) + .autoTrainingTwcsMaxWindows(maxWindows) + .build(); + } + + /** + * As {@link #config} but with a generous window cap, for tests that isolate the sample-size stopping condition. + */ + private static CompressionDictionaryTrainingConfig configTargeting(long desiredAcceptableBytes) + { + return config(desiredAcceptableBytes, 10); + } + + private static long midpoint(long lower, long upper) + { + return lower + (upper - lower) / 2; + } + + /** + * A TWCS table (1-day windows) with four windows (days 1..4), one similarly-sized SSTable per window. + */ + private ColumnFamilyStore fourEqualDayWindows() throws Throwable + { + ColumnFamilyStore cfs = twcsDayWindowTable(); + for (int day = 1; day <= 4; day++) + writeDayWindow(day); + + assertThat(cfs.getLiveSSTables()).as("four windows -> four SSTables").hasSize(4); + return cfs; + } + + /** + * A TWCS table with 1-day windows and auto-compaction disabled (so each flush is its own, window-aligned SSTable). + */ + private ColumnFamilyStore twcsDayWindowTable() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) WITH compaction = " + + "{'class':'TimeWindowCompactionStrategy','compaction_window_unit':'DAYS','compaction_window_size':1}"); + ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); + cfs.disableAutoCompaction(); + return cfs; + } + + /** + * Writes one similarly-sized SSTable whose rows all fall in the given day's TWCS window. + */ + private void writeDayWindow(int day) throws Throwable + { + long base = day * MICROS_PER_DAY; + for (int i = 0; i < ROWS_PER_WINDOW; i++) + execute("INSERT INTO %s (id, v) VALUES (?, ?) USING TIMESTAMP ?", day * 1000 + i, TWO_KIB_VALUE, base + i); + flush(); + } + + private static long maxTimestampOfDay(int day) + { + return day * MICROS_PER_DAY + (ROWS_PER_WINDOW - 1); + } + + /** + * Cumulative uncompressed bytes over the SSTables ordered newest-first: index i = sum of the (i+1) newest. + */ + private static long[] cumulativeUncompressedNewestFirst(ColumnFamilyStore cfs) + { + List sstables = new ArrayList<>(cfs.getLiveSSTables()); + sstables.sort(Comparator.comparingLong(SSTableReader::getMaxTimestamp).reversed()); + + long[] cumulative = new long[sstables.size()]; + long running = 0; + for (int i = 0; i < sstables.size(); i++) + { + running += sstables.get(i).uncompressedLength(); + cumulative[i] = running; + } + return cumulative; + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyIntegrationTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyIntegrationTest.java new file mode 100644 index 000000000000..6f7396de6708 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryAutoTrainingRecencyIntegrationTest.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.compression; + +import java.util.Collections; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingManager.CompressionRatioEvaluator; +import org.apache.cassandra.schema.SystemDistributedKeyspace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * End-to-end test of the recency-biased auto-training loop: it exercises window selection, real data loading, + * a real training + adoption evaluation, and persistence. + *

+ * The data drifts between two TWCS windows - window 1 and window 2 use two disjoint vocabularies - so a + * dictionary trained on the recent window (window 2) compresses recent data markedly better than one trained on the + * old window (window 1). The test: + *

    + *
  1. creates a TWCS, dictionary-compressed, auto-training-enabled table;
  2. + *
  3. loads window 1 and trains the first ("latest") dictionary by hand;
  4. + *
  5. loads window 2 with a different distribution;
  6. + *
  7. starts the real auto-trainer and waits for a run;
  8. + *
  9. asserts the freshly trained dictionary compresses the recent window better than the latest one, and that it + * was persisted (adopted) on top of the old one.
  10. + *
+ */ +public class CompressionDictionaryAutoTrainingRecencyIntegrationTest extends CQLTester +{ + private static final long MICROS_PER_DAY = 86_400L * 1_000_000L; + private static final int ROWS_PER_WINDOW = 1200; + private static final int VOCAB_SIZE = 256; + private static final int PHRASE_LEN = 32; + private static final int PHRASES_PER_ROW = 12; + + private Config.FlushCompression originalFlushCompression; + + @Before + public void useTableFlushCompression() + { + // flush with the table's own (dictionary) compressor at its small chunk length, so the sampled chunks are + // small enough that the dictionary - not in-chunk LZ matching - is what drives the compression ratio + originalFlushCompression = DatabaseDescriptor.getFlushCompression(); + DatabaseDescriptor.setFlushCompression(Config.FlushCompression.table); + } + + @After + public void restoreFlushCompression() + { + DatabaseDescriptor.setFlushCompression(originalFlushCompression); + } + + @Test + public void autoTrainerAdoptsADictionaryThatCompressesTheRecentWindowBetter() throws Throwable + { + String tableName = createTable("CREATE TABLE %s (id int PRIMARY KEY, v text) WITH compression = {" + + "'class':'ZstdDictionaryCompressor'," + + "'auto_training_enabled':'true'," + + "'chunk_length_in_kb':4," + + "'training_max_dictionary_size':'16KiB'," + + "'training_max_total_sample_size':'256KiB'," + + "'auto_training_improvement_threshold':'0.05'} " + + "AND compaction = {'class':'TimeWindowCompactionStrategy'," + + "'compaction_window_unit':'DAYS','compaction_window_size':1}"); + ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(tableName); + cfs.disableAutoCompaction(); // keep each window's flush as its own SSTable + CompressionDictionaryManager manager = cfs.compressionDictionaryManager(); + + // window 1: vocabulary A; hand-train the first ("latest") dictionary on it + writeWindow(1, vocabulary(1)); + manager.train(true, Map.of()); + await("hand-trained dictionary is persisted") + .atMost(30, TimeUnit.SECONDS) + .until(() -> retrieveLatest(cfs) != null); + + CompressionDictionary firstDictionary = retrieveLatest(cfs); + assertThat(firstDictionary).isNotNull(); + long firstDictId = firstDictionary.dictId().id; + + // window 2: a disjoint vocabulary B (the drift), newer than window 1 + writeWindow(2, vocabulary(2)); + + //start the real auto-trainer, constrained to this table, and wait for it to adopt a new dictionary + CompressionDictionaryAutoTrainingManager autoTrainer = new CompressionDictionaryAutoTrainingManager() + { + @Override + Iterable getTables() + { + return Collections.singletonList(cfs); + } + + @Override + boolean isFirstCMSMember() + { + return true; + } + }; + + try + { + autoTrainer.start(0, 1, TimeUnit.SECONDS); + await("auto-trainer trains on the recent window and adopts a new dictionary") + .atMost(60, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(retrieveLatest(cfs).dictId().id) + .as("a new dictionary must be persisted on top of the hand-trained one") + .isNotEqualTo(firstDictId)); + } + finally + { + autoTrainer.close(); + } + + CompressionDictionary adoptedDictionary = retrieveLatest(cfs); + assertThat(adoptedDictionary.dictId().id) + .as("the persisted latest dictionary is the newly adopted one, not the hand-trained one") + .isNotEqualTo(firstDictId); + + // the adopted (recent-window) dictionary compresses the recent window + // better than the hand-trained (old-window) one. + CompressionDictionaryTrainingConfig config = manager.createTrainingConfig(Map.of()); + ColumnFamilyStore.RefViewFragment recentWindow = autoTrainer.resolveViewFragment(cfs); + assertThat(recentWindow).as("the recent window must resolve to a non-null fragment").isNotNull(); + + ICompressionDictionaryTrainer trainer = autoTrainer.createTrainer(cfs); + trainer.start(config); + // CompressionRatioEvaluator.close() closes both the trainer and the fragment + try (CompressionRatioEvaluator evaluator = autoTrainer.createEvaluator(cfs, recentWindow, trainer, config)) + { + double staleRatioOnRecent = evaluator.evaluate(firstDictionary); + double adoptedRatioOnRecent = evaluator.evaluate(adoptedDictionary); + + assertThat(adoptedRatioOnRecent) + .as("dictionary trained on the recent window compresses the recent window better (lower ratio=%s) " + + "than the stale dictionary (ratio=%s)", adoptedRatioOnRecent, staleRatioOnRecent) + .isLessThan(staleRatioOnRecent); + } + } + + private CompressionDictionary retrieveLatest(ColumnFamilyStore cfs) + { + return SystemDistributedKeyspace.retrieveLatestCompressionDictionary(cfs.getKeyspaceName(), + cfs.getTableName(), + cfs.metadata().id.toLongString()); + } + + /** + * Writes {@code ROWS_PER_WINDOW} rows whose timestamps all fall in {@code day}'s TWCS window, then flushes. + */ + private void writeWindow(int day, String[] vocabulary) + { + long base = day * MICROS_PER_DAY; + Random random = new Random(day * 7919L); // deterministic per window + for (int i = 0; i < ROWS_PER_WINDOW; i++) + execute("INSERT INTO %s (id, v) VALUES (?, ?) USING TIMESTAMP ?", day * 100_000 + i, row(vocabulary, random), base + i); + flush(); + } + + /** + * A row value: {@code PHRASES_PER_ROW} space-joined phrases drawn (with repetition) from the window's vocabulary. + */ + private static String row(String[] vocabulary, Random random) + { + StringBuilder sb = new StringBuilder(PHRASES_PER_ROW * (PHRASE_LEN + 1)); + for (int p = 0; p < PHRASES_PER_ROW; p++) + { + if (p > 0) + sb.append(' '); + sb.append(vocabulary[random.nextInt(vocabulary.length)]); + } + return sb.toString(); + } + + /** + * A deterministic vocabulary of {@code VOCAB_SIZE} distinct {@code PHRASE_LEN}-char phrases. Different seeds yield + * effectively disjoint vocabularies (random 32-char strings collide with negligible probability), which is what + * makes a dictionary trained on one window compress the other window poorly. + */ + private static String[] vocabulary(long seed) + { + Random random = new Random(seed); + String[] vocabulary = new String[VOCAB_SIZE]; + for (int i = 0; i < VOCAB_SIZE; i++) + { + StringBuilder sb = new StringBuilder(PHRASE_LEN); + for (int c = 0; c < PHRASE_LEN; c++) + sb.append((char) ('a' + random.nextInt(26))); + vocabulary[i] = sb.toString(); + } + return vocabulary; + } +} diff --git a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryCacheTest.java b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryCacheTest.java index 99ef5592ec52..72229cd88e71 100644 --- a/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryCacheTest.java +++ b/test/unit/org/apache/cassandra/db/compression/CompressionDictionaryCacheTest.java @@ -18,6 +18,11 @@ package org.apache.cassandra.db.compression; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -398,6 +403,92 @@ public void testGetCurrentReturnsNullAfterExpiration() throws InterruptedExcepti } } + // Regression tests for CASSANDRA-21047 dictionary-ref leak (ZstdCompressionDictionary$Tidy): + // add() must return the CANONICAL cached instance so callers never reference (and tryRef) a + // "loser" instance whose lazily-created selfRef the cache does not own and therefore never releases. + + @Test + public void testAddReturnsCanonicalInstanceForDuplicateId() + { + ZstdCompressionDictionary winner = createTestDictionary(2); + ZstdCompressionDictionary loser = createTestDictionary(2); // same DictId, distinct instance + + assertThat(cache.add(winner)) + .as("add() returns the instance it caches on a miss") + .isSameAs(winner); + + assertThat(cache.add(loser)) + .as("add() of a duplicate-id instance must return the canonical cached winner, not the loser") + .isSameAs(winner); + + assertThat(loser.selfRef()) + .as("the losing instance must never acquire a selfRef the cache does not own (else it leaks)") + .isNull(); + + closeQuietly(loser); + } + + @Test + public void testConcurrentAddSameIdReturnsSingleCanonicalInstance() throws InterruptedException + { + final int threads = 16; + ExecutorService pool = Executors.newFixedThreadPool(threads); + try + { + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threads); + List created = Collections.synchronizedList(new ArrayList<>()); + Set returned = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + for (int i = 0; i < threads; i++) + { + pool.submit(() -> { + ZstdCompressionDictionary d = createTestDictionary(42); // all race on the same DictId + created.add(d); + try + { + start.await(); + returned.add(cache.add(d)); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + finally + { + done.countDown(); + } + }); + } + + start.countDown(); // release all threads at once to maximise the race + assertThat(done.await(30, TimeUnit.SECONDS)).as("all add() threads finished").isTrue(); + + assertThat(returned) + .as("all concurrent add()s of the same dictId converge on exactly one canonical instance") + .hasSize(1); + + CompressionDictionary canonical = returned.iterator().next(); + assertThat(canonical.selfRef()).as("the canonical winner owns a selfRef").isNotNull(); + + int withSelfRef = 0; + for (ZstdCompressionDictionary d : created) + if (d.selfRef() != null) + withSelfRef++; + assertThat(withSelfRef) + .as("only the cached winner may own a selfRef; every loser must be selfRef==null (no leak)") + .isEqualTo(1); + + for (ZstdCompressionDictionary d : created) + if (d != canonical) + closeQuietly(d); + } + finally + { + pool.shutdownNow(); + } + } + private static ZstdCompressionDictionary createTestDictionary(long id) { try diff --git a/test/unit/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTableTest.java b/test/unit/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTableTest.java new file mode 100644 index 000000000000..d2969932eb3d --- /dev/null +++ b/test/unit/org/apache/cassandra/db/virtual/CompressionDictionaryAutoTrainingTableTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.virtual; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.compression.CompressionDictionary; +import org.apache.cassandra.db.compression.CompressionDictionaryAutoTrainingHistory; +import org.apache.cassandra.utils.FBUtilities; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that auto-training decisions recorded by {@link CompressionDictionaryAutoTrainingHistory} are visible + * through the virtual table, newest first. + */ +public class CompressionDictionaryAutoTrainingTableTest extends CQLTester +{ + private static final String KS = "vts_auto_training"; + + @BeforeClass + public static void setUpClass() + { + CQLTester.setUpClass(); + VirtualKeyspaceRegistry.instance.register( + new VirtualKeyspace(KS, ImmutableList.of(new CompressionDictionaryAutoTrainingTable(KS)))); + } + + @Before + public void clearHistory() + { + CompressionDictionaryAutoTrainingHistory.instance.clear(); + } + + @Test + public void emptyHistoryYieldsNoRows() throws Throwable + { + assertEmpty(execute(select())); + } + + @Test + public void decisionIsVisibleWithItsRatiosThresholdAndOutcome() throws Throwable + { + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.30, 0.40, 0.15, true); + + UntypedResultSet result = execute(select()); + assertThat(result.size()).isEqualTo(1); + + UntypedResultSet.Row row = result.one(); + assertThat(row.getString("keyspace_name")).isEqualTo("ks1"); + assertThat(row.getString("table_name")).isEqualTo("tbl1"); + assertThat(row.getTimestamp("trained_at").getTime()).isEqualTo(1000L); + assertThat(row.getInetAddress("node")).isEqualTo(FBUtilities.getBroadcastAddressAndPort().getAddress()); + assertThat(row.getString("kind")).isEqualTo(CompressionDictionary.Kind.ZSTD.name()); + assertThat(row.getDouble("baseline_ratio")).isEqualTo(0.50); + assertThat(row.getDouble("candidate_ratio")).isEqualTo(0.30); + assertThat(row.getDouble("improvement")).isEqualTo(0.40); + assertThat(row.getDouble("threshold")).isEqualTo(0.15); + assertThat(row.getBoolean("promoted")).isTrue(); + } + + @Test + public void rejectedCandidateIsRecordedToo() throws Throwable + { + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.49, 0.02, 0.15, false); + + UntypedResultSet.Row row = execute(select()).one(); + assertThat(row.getDouble("improvement")).isEqualTo(0.02); + assertThat(row.getBoolean("promoted")).isFalse(); + } + + @Test + public void decisionsForOneTableComeBackNewestFirst() throws Throwable + { + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.49, 0.02, 0.15, false); + CompressionDictionaryAutoTrainingHistory.instance.record(3000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.30, 0.40, 0.15, true); + CompressionDictionaryAutoTrainingHistory.instance.record(2000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.45, 0.10, 0.15, false); + + List times = new ArrayList<>(); + for (UntypedResultSet.Row row : execute(select() + " WHERE keyspace_name = 'ks1' AND table_name = 'tbl1'")) + times.add(row.getTimestamp("trained_at").getTime()); + + assertThat(times).containsExactly(3000L, 2000L, 1000L); + } + + @Test + public void decisionsArePartitionedByKeyspace() throws Throwable + { + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.50, 0.30, 0.40, 0.15, true); + CompressionDictionaryAutoTrainingHistory.instance.record(1000L, "ks2", "tbl2", CompressionDictionary.Kind.ZSTD, 0.60, 0.20, 0.66, 0.15, true); + + assertThat(execute(select()).size()).isEqualTo(2); + + UntypedResultSet ks2 = execute(select() + " WHERE keyspace_name = 'ks2'"); + assertThat(ks2.size()).isEqualTo(1); + assertThat(ks2.one().getString("table_name")).isEqualTo("tbl2"); + } + + @Test + public void oldestEntryIsEvictedOnceTheBoundIsReached() + { + CompressionDictionaryAutoTrainingHistory history = new CompressionDictionaryAutoTrainingHistory(2); + history.record(1000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.5, 0.4, 0.2, 0.15, true); + history.record(2000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.5, 0.4, 0.2, 0.15, true); + history.record(3000L, "ks1", "tbl1", CompressionDictionary.Kind.ZSTD, 0.5, 0.4, 0.2, 0.15, true); + + assertThat(history.entries()).hasSize(2); + assertThat(history.entries().get(0).timestampMillis).isEqualTo(3000L); + assertThat(history.entries().get(1).timestampMillis).isEqualTo(2000L); + } + + private static String select() + { + return "SELECT * FROM " + KS + '.' + CompressionDictionaryAutoTrainingTable.TABLE_NAME; + } +} diff --git a/test/unit/org/apache/cassandra/io/compress/ZstdCompressorTest.java b/test/unit/org/apache/cassandra/io/compress/ZstdCompressorTest.java index 0c5c5d46e138..1fdd0d06c1e5 100644 --- a/test/unit/org/apache/cassandra/io/compress/ZstdCompressorTest.java +++ b/test/unit/org/apache/cassandra/io/compress/ZstdCompressorTest.java @@ -18,14 +18,25 @@ package org.apache.cassandra.io.compress; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import com.github.luben.zstd.Zstd; import com.google.common.collect.ImmutableMap; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * Zstd Compressor specific tests. General compressor tests are in {@link CompressorTest} @@ -50,4 +61,139 @@ public void badCompressionLevelParamThrowsExceptionMax() { ZstdCompressor.create(ImmutableMap.of(ZstdCompressor.COMPRESSION_LEVEL_OPTION_NAME, Integer.toString(Zstd.maxCompressionLevel() + 1))); } + + /** + * Compression and decompression borrow a pooled native context instead of allocating one per chunk, so a run of + * chunks must leave exactly one context in each pool - created once, returned after every call. + */ + @Test + public void contextsAreReusedAcrossChunks() throws Exception + { + // a level of its own, so the shared per-level instance is not one another test has already exercised + ZstdCompressor compressor = ZstdCompressor.getOrCreate(7); + assertEquals("no context is allocated before the first chunk", 0, compressor.pooledCompressContexts()); + assertEquals(0, compressor.pooledDecompressContexts()); + + for (int i = 0; i < 16; i++) + roundTrip(compressor, chunk(4096, i)); + + assertEquals("one compression context, reused across all chunks", 1, compressor.pooledCompressContexts()); + assertEquals("one decompression context, reused across all chunks", 1, compressor.pooledDecompressContexts()); + } + + /** + * Pooling must not change the bytes written: a frame produced through a pooled context has to stay readable by + * the one-shot API this compressor used previously, which is what on-disk data was written with. + */ + @Test + public void framesRemainReadableByTheOneShotApi() throws Exception + { + ZstdCompressor compressor = ZstdCompressor.getOrCreate(8); + byte[] data = chunk(8192, 42); + + ByteBuffer input = direct(data); + ByteBuffer compressed = ByteBuffer.allocateDirect(compressor.initialCompressedBufferLength(data.length)); + compressor.compress(input, compressed); + compressed.flip(); + + ByteBuffer output = ByteBuffer.allocateDirect(data.length); + Zstd.decompress(output, compressed); + output.flip(); + + byte[] result = new byte[output.remaining()]; + output.get(result); + assertArrayEquals(data, result); + } + + /** The byte[] decompression path is pooled too, and must round-trip what the ByteBuffer path produced. */ + @Test + public void byteArrayDecompressionRoundTrips() throws Exception + { + ZstdCompressor compressor = ZstdCompressor.getOrCreate(9); + byte[] data = chunk(2048, 7); + + ByteBuffer compressed = ByteBuffer.allocateDirect(compressor.initialCompressedBufferLength(data.length)); + compressor.compress(direct(data), compressed); + compressed.flip(); + + byte[] compressedBytes = new byte[compressed.remaining()]; + compressed.get(compressedBytes); + + byte[] output = new byte[data.length]; + int size = compressor.uncompress(compressedBytes, 0, compressedBytes.length, output, 0); + + assertEquals(data.length, size); + assertArrayEquals(data, output); + } + + /** + * Contexts are not thread safe, so each call borrows one exclusively. Concurrent callers must all round-trip + * correctly, and the pool must not grow beyond the number of threads that were ever in flight at once. + */ + @Test + public void concurrentCallersDoNotShareAContext() throws Exception + { + ZstdCompressor compressor = ZstdCompressor.getOrCreate(10); + int threads = 8; + int chunksPerThread = 40; + + ExecutorService executor = Executors.newFixedThreadPool(threads); + try + { + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) + { + int seed = t; + tasks.add(() -> { + for (int i = 0; i < chunksPerThread; i++) + roundTrip(compressor, chunk(4096, seed * 1000 + i)); + return null; + }); + } + + for (Future future : executor.invokeAll(tasks)) + future.get(2, TimeUnit.MINUTES); // surfaces any corruption or native failure + } + finally + { + executor.shutdownNow(); + } + + assertTrue("the pool must not exceed the peak concurrency, was " + compressor.pooledCompressContexts(), + compressor.pooledCompressContexts() <= threads); + assertTrue(compressor.pooledDecompressContexts() <= threads); + } + + private static void roundTrip(ZstdCompressor compressor, byte[] data) throws Exception + { + ByteBuffer compressed = ByteBuffer.allocateDirect(compressor.initialCompressedBufferLength(data.length)); + compressor.compress(direct(data), compressed); + compressed.flip(); + + ByteBuffer output = ByteBuffer.allocateDirect(data.length); + compressor.uncompress(compressed, output); + output.flip(); + + byte[] result = new byte[output.remaining()]; + output.get(result); + assertArrayEquals(data, result); + } + + private static ByteBuffer direct(byte[] data) + { + ByteBuffer buffer = ByteBuffer.allocateDirect(data.length); + buffer.put(data); + buffer.flip(); + return buffer; + } + + /** Compressible but not trivial content, so the frame is a real one rather than a degenerate case. */ + private static byte[] chunk(int size, int seed) + { + Random random = new Random(seed); + byte[] data = new byte[size]; + for (int i = 0; i < size; i++) + data[i] = (byte) ('a' + random.nextInt(8)); + return data; + } } diff --git a/test/unit/org/apache/cassandra/io/compress/ZstdDictionaryCompressorTest.java b/test/unit/org/apache/cassandra/io/compress/ZstdDictionaryCompressorTest.java index 7439eaea8fbb..60f7f021b822 100644 --- a/test/unit/org/apache/cassandra/io/compress/ZstdDictionaryCompressorTest.java +++ b/test/unit/org/apache/cassandra/io/compress/ZstdDictionaryCompressorTest.java @@ -26,6 +26,7 @@ import com.github.luben.zstd.Zstd; import com.github.luben.zstd.ZstdDictTrainer; +import com.google.common.collect.ImmutableMap; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -75,6 +76,44 @@ public static void tearDown() } } + /** + * A compressor with no dictionary yet - the state every dictionary-compressed table is in until one is trained - + * falls through to the implementations inherited from ZstdCompressorBase, so it must pool native contexts just + * as the plain compressor does. This is why the pooling lives in the base rather than in ZstdCompressor. + */ + @Test + public void dictionarylessCompressorPoolsNativeContexts() throws Exception + { + // a level of its own, so the per-level cached instance is not one another test has already exercised + ZstdDictionaryCompressor compressor = + ZstdDictionaryCompressor.create(ImmutableMap.of(ZstdCompressorBase.COMPRESSION_LEVEL_OPTION_NAME, "11")); + + assertThat(compressor.pooledCompressContexts()).describedAs("no context before the first chunk").isZero(); + assertThat(compressor.pooledDecompressContexts()).isZero(); + + for (int i = 0; i < 16; i++) + { + ByteBuffer input = ByteBuffer.allocateDirect(compressibleData.length); + input.put(compressibleData); + input.flip(); + + ByteBuffer compressed = ByteBuffer.allocateDirect(compressor.initialCompressedBufferLength(compressibleData.length)); + compressor.compress(input, compressed); + compressed.flip(); + + ByteBuffer decompressed = ByteBuffer.allocateDirect(compressibleData.length); + compressor.uncompress(compressed, decompressed); + decompressed.flip(); + + byte[] result = new byte[decompressed.remaining()]; + decompressed.get(result); + assertThat(result).isEqualTo(compressibleData); + } + + assertThat(compressor.pooledCompressContexts()).describedAs("one compression context, reused").isEqualTo(1); + assertThat(compressor.pooledDecompressContexts()).describedAs("one decompression context, reused").isEqualTo(1); + } + @Test public void testCreateWithOptions() { diff --git a/test/unit/org/apache/cassandra/schema/SystemDistributedKeyspaceCompressionDictionaryTest.java b/test/unit/org/apache/cassandra/schema/SystemDistributedKeyspaceCompressionDictionaryTest.java index 2a3fd248621a..10ac13686390 100644 --- a/test/unit/org/apache/cassandra/schema/SystemDistributedKeyspaceCompressionDictionaryTest.java +++ b/test/unit/org/apache/cassandra/schema/SystemDistributedKeyspaceCompressionDictionaryTest.java @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; +import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.QueryProcessor; @@ -32,6 +33,11 @@ import org.apache.cassandra.db.compression.CompressionDictionary.Kind; import org.apache.cassandra.db.compression.ZstdCompressionDictionary; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + import static org.assertj.core.api.Assertions.assertThat; public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTester @@ -182,6 +188,31 @@ public void testRetrieveNonExistentDictionary() .isNull(); } + @Test + public void testMissingDictionaryDoesNotWarn() + { + Logger logger = (Logger) LoggerFactory.getLogger(SystemDistributedKeyspace.class); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + + try + { + assertThat(SystemDistributedKeyspace.retrieveLatestCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, "nonexistingid")).isNull(); + assertThat(SystemDistributedKeyspace.retrieveLightweightLatestCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, "nonexistingid")).isNull(); + assertThat(SystemDistributedKeyspace.retrieveCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, "nonexistingid", 999L)).isNull(); + + assertThat(appender.list) + .as("a table with no dictionary yet is an ordinary state, not a read failure, and must not warn") + .noneMatch(event -> event.getLevel() == Level.WARN); + } + finally + { + logger.detachAppender(appender); + appender.stop(); + } + } + @Test public void testStoredDictionaryIncludesLengthAndChecksum() {