Merge remote-tracking branch 'origin/master' into pr-local-plan

This commit is contained in:
Igor Nikonov 2024-06-18 11:12:40 +00:00
commit 7c82f45fb0
142 changed files with 6150 additions and 2086 deletions

View File

@ -48,8 +48,7 @@ At a minimum, the following information should be added (but add more as needed)
- [ ] <!---ci_include_stateful--> Allow: Stateful tests - [ ] <!---ci_include_stateful--> Allow: Stateful tests
- [ ] <!---ci_include_integration--> Allow: Integration Tests - [ ] <!---ci_include_integration--> Allow: Integration Tests
- [ ] <!---ci_include_performance--> Allow: Performance tests - [ ] <!---ci_include_performance--> Allow: Performance tests
- [ ] <!---ci_set_normal_builds--> Allow: Normal Builds - [ ] <!---ci_set_builds--> Allow: All Builds
- [ ] <!---ci_set_special_builds--> Allow: Special Builds
- [ ] <!---ci_set_non_required--> Allow: All NOT Required Checks - [ ] <!---ci_set_non_required--> Allow: All NOT Required Checks
- [ ] <!---batch_0_1--> Allow: batch 1, 2 for multi-batch jobs - [ ] <!---batch_0_1--> Allow: batch 1, 2 for multi-batch jobs
- [ ] <!---batch_2_3--> Allow: batch 3, 4, 5, 6 for multi-batch jobs - [ ] <!---batch_2_3--> Allow: batch 3, 4, 5, 6 for multi-batch jobs

View File

@ -70,7 +70,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Compatibility check (amd64) test_name: Compatibility check (release)
runner_type: style-checker runner_type: style-checker
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
CompatibilityCheckAarch64: CompatibilityCheckAarch64:
@ -194,7 +194,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Install packages (amd64) test_name: Install packages (release)
runner_type: style-checker runner_type: style-checker
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
run_command: | run_command: |
@ -204,7 +204,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Install packages (arm64) test_name: Install packages (aarch64)
runner_type: style-checker-aarch64 runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
run_command: | run_command: |

View File

@ -115,25 +115,16 @@ jobs:
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
################################# Reports ################################# ################################# Reports #################################
# Reports should be run even if Builds_1/2 failed - put them separately in wf (not in Tests_1/2) # Reports should run even if Builds_1/2 fail - run them separately, not in Tests_1/2/3
Builds_1_Report: Builds_Report:
# run report check for failed builds to indicate the CI error # run report check for failed builds to indicate the CI error
if: ${{ !cancelled() && needs.RunConfig.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse build check') }} if: ${{ !cancelled() && needs.RunConfig.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse build check') }}
needs: [RunConfig, Builds_1] needs: [RunConfig, Builds_1, Builds_2]
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: ClickHouse build check test_name: ClickHouse build check
runner_type: style-checker-aarch64 runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
Builds_2_Report:
# run report check for failed builds to indicate the CI error
if: ${{ !cancelled() && needs.RunConfig.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse special build check') }}
needs: [RunConfig, Builds_2]
uses: ./.github/workflows/reusable_test.yml
with:
test_name: ClickHouse special build check
runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }}
MarkReleaseReady: MarkReleaseReady:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
@ -165,7 +156,7 @@ jobs:
FinishCheck: FinishCheck:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
needs: [RunConfig, Builds_1, Builds_2, Builds_1_Report, Builds_2_Report, Tests_1, Tests_2, Tests_3] needs: [RunConfig, Builds_1, Builds_2, Builds_Report, Tests_1, Tests_2, Tests_3]
runs-on: [self-hosted, style-checker-aarch64] runs-on: [self-hosted, style-checker-aarch64]
steps: steps:
- name: Check out repository code - name: Check out repository code

View File

@ -143,29 +143,20 @@ jobs:
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
################################# Reports ################################# ################################# Reports #################################
# Reports should by run even if Builds_1/2 fail, so put them separately in wf (not in Tests_1/2) # Reports should run even if Builds_1/2 fail - run them separately (not in Tests_1/2/3)
Builds_1_Report: Builds_Report:
# run report check for failed builds to indicate the CI error # run report check for failed builds to indicate the CI error
if: ${{ !cancelled() && needs.StyleCheck.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse build check') }} if: ${{ !cancelled() && needs.RunConfig.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse build check') }}
needs: [RunConfig, StyleCheck, Builds_1] needs: [RunConfig, StyleCheck, Builds_1, Builds_2]
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: ClickHouse build check test_name: ClickHouse build check
runner_type: style-checker-aarch64 runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
Builds_2_Report:
# run report check for failed builds to indicate the CI error
if: ${{ !cancelled() && needs.StyleCheck.result == 'success' && contains(fromJson(needs.RunConfig.outputs.data).jobs_data.jobs_to_do, 'ClickHouse special build check') }}
needs: [RunConfig, StyleCheck, Builds_2]
uses: ./.github/workflows/reusable_test.yml
with:
test_name: ClickHouse special build check
runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }}
CheckReadyForMerge: CheckReadyForMerge:
if: ${{ !cancelled() && needs.StyleCheck.result == 'success' }} if: ${{ !cancelled() && needs.StyleCheck.result == 'success' }}
needs: [RunConfig, BuildDockers, StyleCheck, FastTest, Builds_1, Builds_2, Builds_1_Report, Builds_2_Report, Tests_1, Tests_2] needs: [RunConfig, BuildDockers, StyleCheck, FastTest, Builds_1, Builds_2, Builds_Report, Tests_1, Tests_2]
runs-on: [self-hosted, style-checker-aarch64] runs-on: [self-hosted, style-checker-aarch64]
steps: steps:
- name: Check out repository code - name: Check out repository code
@ -181,7 +172,7 @@ jobs:
# #
FinishCheck: FinishCheck:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
needs: [RunConfig, BuildDockers, StyleCheck, FastTest, Builds_1, Builds_2, Builds_1_Report, Builds_2_Report, Tests_1, Tests_2, Tests_3] needs: [RunConfig, BuildDockers, StyleCheck, FastTest, Builds_1, Builds_2, Builds_Report, Tests_1, Tests_2, Tests_3]
runs-on: [self-hosted, style-checker-aarch64] runs-on: [self-hosted, style-checker-aarch64]
steps: steps:
- name: Check out repository code - name: Check out repository code

View File

@ -65,7 +65,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Compatibility check (amd64) test_name: Compatibility check (release)
runner_type: style-checker runner_type: style-checker
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
CompatibilityCheckAarch64: CompatibilityCheckAarch64:
@ -244,7 +244,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Install packages (amd64) test_name: Install packages (release)
runner_type: style-checker runner_type: style-checker
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
run_command: | run_command: |
@ -254,7 +254,7 @@ jobs:
if: ${{ !failure() && !cancelled() }} if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/reusable_test.yml uses: ./.github/workflows/reusable_test.yml
with: with:
test_name: Install packages (arm64) test_name: Install packages (aarch64)
runner_type: style-checker-aarch64 runner_type: style-checker-aarch64
data: ${{ needs.RunConfig.outputs.data }} data: ${{ needs.RunConfig.outputs.data }}
run_command: | run_command: |

View File

@ -89,10 +89,6 @@ function configure()
# since we run clickhouse from root # since we run clickhouse from root
sudo chown root: /var/lib/clickhouse sudo chown root: /var/lib/clickhouse
# Set more frequent update period of asynchronous metrics to more frequently update information about real memory usage (less chance of OOM).
echo "<clickhouse><asynchronous_metrics_update_period_s>1</asynchronous_metrics_update_period_s></clickhouse>" \
> /etc/clickhouse-server/config.d/asynchronous_metrics_update_period_s.xml
local total_mem local total_mem
total_mem=$(awk '/MemTotal/ { print $(NF-1) }' /proc/meminfo) # KiB total_mem=$(awk '/MemTotal/ { print $(NF-1) }' /proc/meminfo) # KiB
total_mem=$(( total_mem*1024 )) # bytes total_mem=$(( total_mem*1024 )) # bytes

View File

@ -1490,6 +1490,8 @@ Differs from [PrettySpaceNoEscapes](#prettyspacenoescapes) in that up to 10,000
- [output_format_pretty_color](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_color) - use ANSI escape sequences to paint colors in Pretty formats. Default value - `true`. - [output_format_pretty_color](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_color) - use ANSI escape sequences to paint colors in Pretty formats. Default value - `true`.
- [output_format_pretty_grid_charset](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_grid_charset) - Charset for printing grid borders. Available charsets: ASCII, UTF-8. Default value - `UTF-8`. - [output_format_pretty_grid_charset](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_grid_charset) - Charset for printing grid borders. Available charsets: ASCII, UTF-8. Default value - `UTF-8`.
- [output_format_pretty_row_numbers](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_row_numbers) - Add row numbers before each row for pretty output format. Default value - `true`. - [output_format_pretty_row_numbers](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_row_numbers) - Add row numbers before each row for pretty output format. Default value - `true`.
- [output_format_pretty_display_footer_column_names](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_display_footer_column_names) - Display column names in the footer if table contains many rows. Default value - `true`.
- [output_format_pretty_display_footer_column_names_min_rows](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_display_footer_column_names_min_rows) - Sets the minimum number of rows for which a footer will be displayed if [output_format_pretty_display_footer_column_names](/docs/en/operations/settings/settings-formats.md/#output_format_pretty_display_footer_column_names) is enabled. Default value - 50.
## RowBinary {#rowbinary} ## RowBinary {#rowbinary}

View File

@ -508,7 +508,7 @@ Now `rule` can configure `method`, `headers`, `url`, `handler`:
- `headers` are responsible for matching the header part of the HTTP request. It is compatible with RE2s regular expressions. It is an optional configuration. If it is not defined in the configuration file, it does not match the header portion of the HTTP request. - `headers` are responsible for matching the header part of the HTTP request. It is compatible with RE2s regular expressions. It is an optional configuration. If it is not defined in the configuration file, it does not match the header portion of the HTTP request.
- `handler` contains the main processing part. Now `handler` can configure `type`, `status`, `content_type`, `response_content`, `query`, `query_param_name`. - `handler` contains the main processing part. Now `handler` can configure `type`, `status`, `content_type`, `http_response_headers`, `response_content`, `query`, `query_param_name`.
`type` currently supports three types: [predefined_query_handler](#predefined_query_handler), [dynamic_query_handler](#dynamic_query_handler), [static](#static). `type` currently supports three types: [predefined_query_handler](#predefined_query_handler), [dynamic_query_handler](#dynamic_query_handler), [static](#static).
- `query` — use with `predefined_query_handler` type, executes query when the handler is called. - `query` — use with `predefined_query_handler` type, executes query when the handler is called.
@ -519,6 +519,8 @@ Now `rule` can configure `method`, `headers`, `url`, `handler`:
- `content_type` — use with any type, response [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). - `content_type` — use with any type, response [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
- `http_response_headers` — use with any type, response headers map. Could be used to set content type as well.
- `response_content` — use with `static` type, response content sent to client, when using the prefix file:// or config://, find the content from the file or configuration sends to client. - `response_content` — use with `static` type, response content sent to client, when using the prefix file:// or config://, find the content from the file or configuration sends to client.
Next are the configuration methods for different `type`. Next are the configuration methods for different `type`.
@ -616,6 +618,33 @@ Return a message.
<type>static</type> <type>static</type>
<status>402</status> <status>402</status>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<Content-Language>en</Content-Language>
<X-My-Custom-Header>43</X-My-Custom-Header>
</http_response_headers>
<response_content>Say Hi!</response_content>
</handler>
</rule>
<defaults/>
</http_handlers>
```
`http_response_headers` could be used to set content type instead of `content_type`.
``` xml
<http_handlers>
<rule>
<methods>GET</methods>
<headers><XXX>xxx</XXX></headers>
<url>/hi</url>
<handler>
<type>static</type>
<status>402</status>
<http_response_headers>
<Content-Type>text/html; charset=UTF-8</Content-Type>
<Content-Language>en</Content-Language>
<X-My-Custom-Header>43</X-My-Custom-Header>
</http_response_headers>
<response_content>Say Hi!</response_content> <response_content>Say Hi!</response_content>
</handler> </handler>
</rule> </rule>
@ -696,6 +725,9 @@ Find the content from the file send to client.
<handler> <handler>
<type>static</type> <type>static</type>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<ETag>737060cd8c284d8af7ad3082f209582d</ETag>
</http_response_headers>
<response_content>file:///absolute_path_file.html</response_content> <response_content>file:///absolute_path_file.html</response_content>
</handler> </handler>
</rule> </rule>
@ -706,6 +738,9 @@ Find the content from the file send to client.
<handler> <handler>
<type>static</type> <type>static</type>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<ETag>737060cd8c284d8af7ad3082f209582d</ETag>
</http_response_headers>
<response_content>file://./relative_path_file.html</response_content> <response_content>file://./relative_path_file.html</response_content>
</handler> </handler>
</rule> </rule>

View File

@ -3084,3 +3084,21 @@ This setting is only necessary for the migration period and will become obsolete
Type: Bool Type: Bool
Default: 1 Default: 1
## merge_workload {#merge_workload}
Used to regulate how resources are utilized and shared between merges and other workloads. Specified value is used as `workload` setting value for all background merges. Can be overridden by a merge tree setting.
Default value: "default"
**See Also**
- [Workload Scheduling](/docs/en/operations/workload-scheduling.md)
## mutation_workload {#mutation_workload}
Used to regulate how resources are utilized and shared between mutations and other workloads. Specified value is used as `workload` setting value for all background mutations. Can be overridden by a merge tree setting.
Default value: "default"
**See Also**
- [Workload Scheduling](/docs/en/operations/workload-scheduling.md)

View File

@ -974,6 +974,24 @@ Default value: false
- [exclude_deleted_rows_for_part_size_in_merge](#exclude_deleted_rows_for_part_size_in_merge) setting - [exclude_deleted_rows_for_part_size_in_merge](#exclude_deleted_rows_for_part_size_in_merge) setting
## merge_workload
Used to regulate how resources are utilized and shared between merges and other workloads. Specified value is used as `workload` setting value for background merges of this table. If not specified (empty string), then server setting `merge_workload` is used instead.
Default value: an empty string
**See Also**
- [Workload Scheduling](/docs/en/operations/workload-scheduling.md)
## mutation_workload
Used to regulate how resources are utilized and shared between mutations and other workloads. Specified value is used as `workload` setting value for background mutations of this table. If not specified (empty string), then server setting `mutation_workload` is used instead.
Default value: an empty string
**See Also**
- [Workload Scheduling](/docs/en/operations/workload-scheduling.md)
### optimize_row_order ### optimize_row_order
Controls if the row order should be optimized during inserts to improve the compressability of the newly inserted table part. Controls if the row order should be optimized during inserts to improve the compressability of the newly inserted table part.

View File

@ -1706,6 +1706,43 @@ Result:
└────────────┘ └────────────┘
``` ```
## output_format_pretty_display_footer_column_names
Display column names in the footer if there are many table rows.
Possible values:
- 0 — No column names are displayed in the footer.
- 1 — Column names are displayed in the footer if row count is greater than or equal to the threshold value set by [output_format_pretty_display_footer_column_names_min_rows](#output_format_pretty_display_footer_column_names_min_rows) (50 by default).
Default value: `1`.
**Example**
Query:
```sql
SELECT *, toTypeName(*) FROM (SELECT * FROM system.numbers LIMIT 1000);
```
Result:
```response
┌─number─┬─toTypeName(number)─┐
1. │ 0 │ UInt64 │
2. │ 1 │ UInt64 │
3. │ 2 │ UInt64 │
...
999. │ 998 │ UInt64 │
1000. │ 999 │ UInt64 │
└─number─┴─toTypeName(number)─┘
```
## output_format_pretty_display_footer_column_names_min_rows
Sets the minimum number of rows for which a footer with column names will be displayed if setting [output_format_pretty_display_footer_column_names](#output_format_pretty_display_footer_column_names) is enabled.
Default value: `50`.
## Template format settings {#template-format-settings} ## Template format settings {#template-format-settings}
### format_template_resultset {#format_template_resultset} ### format_template_resultset {#format_template_resultset}

View File

@ -47,6 +47,8 @@ Example:
Queries can be marked with setting `workload` to distinguish different workloads. If `workload` is not set, than value "default" is used. Note that you are able to specify the other value using settings profiles. Setting constraints can be used to make `workload` constant if you want all queries from the user to be marked with fixed value of `workload` setting. Queries can be marked with setting `workload` to distinguish different workloads. If `workload` is not set, than value "default" is used. Note that you are able to specify the other value using settings profiles. Setting constraints can be used to make `workload` constant if you want all queries from the user to be marked with fixed value of `workload` setting.
It is possible to assign a `workload` setting for background activities. Merges and mutations are using `merge_workload` and `mutation_workload` server settings correspondingly. These values can also be overridden for specific tables using `merge_workload` and `mutation_workload` merge tree settings
Let's consider an example of a system with two different workloads: "production" and "development". Let's consider an example of a system with two different workloads: "production" and "development".
```sql ```sql
@ -151,6 +153,9 @@ Example:
</clickhouse> </clickhouse>
``` ```
## See also ## See also
- [system.scheduler](/docs/en/operations/system-tables/scheduler.md) - [system.scheduler](/docs/en/operations/system-tables/scheduler.md)
- [merge_workload](/docs/en/operations/settings/merge-tree-settings.md#merge_workload) merge tree setting
- [merge_workload](/docs/en/operations/server-configuration-parameters/settings.md#merge_workload) global server setting
- [mutation_workload](/docs/en/operations/settings/merge-tree-settings.md#mutation_workload) merge tree setting
- [mutation_workload](/docs/en/operations/server-configuration-parameters/settings.md#mutation_workload) global server setting

View File

@ -414,6 +414,8 @@ $ curl -v 'http://localhost:8123/predefined_query'
- `content_type` — используется со всеми типами, возвращает [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). - `content_type` — используется со всеми типами, возвращает [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
- `http_response_headers` — используется со всеми типами чтобы добавить кастомные хедеры в ответ. Может использоваться в том числе для задания хедера `Content-Type` вместо `content_type`.
- `response_content` — используется с типом`static`, содержимое ответа, отправленное клиенту, при использовании префикса file:// or config://, находит содержимое из файла или конфигурации, отправленного клиенту. - `response_content` — используется с типом`static`, содержимое ответа, отправленное клиенту, при использовании префикса file:// or config://, находит содержимое из файла или конфигурации, отправленного клиенту.
Далее приведены методы настройки для различных типов. Далее приведены методы настройки для различных типов.
@ -509,6 +511,33 @@ max_final_threads 2
<type>static</type> <type>static</type>
<status>402</status> <status>402</status>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<Content-Language>en</Content-Language>
<X-My-Custom-Header>43</X-My-Custom-Header>
</http_response_headers>
<response_content>Say Hi!</response_content>
</handler>
</rule>
<defaults/>
</http_handlers>
```
`http_response_headers` так же может использоваться для определения `Content-Type` вместо `content_type`.
``` xml
<http_handlers>
<rule>
<methods>GET</methods>
<headers><XXX>xxx</XXX></headers>
<url>/hi</url>
<handler>
<type>static</type>
<status>402</status>
<http_response_headers>
<Content-Type>text/html; charset=UTF-8</Content-Type>
<Content-Language>en</Content-Language>
<X-My-Custom-Header>43</X-My-Custom-Header>
</http_response_headers>
<response_content>Say Hi!</response_content> <response_content>Say Hi!</response_content>
</handler> </handler>
</rule> </rule>
@ -589,6 +618,9 @@ $ curl -v -H 'XXX:xxx' 'http://localhost:8123/get_config_static_handler'
<handler> <handler>
<type>static</type> <type>static</type>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<ETag>737060cd8c284d8af7ad3082f209582d</ETag>
</http_response_headers>
<response_content>file:///absolute_path_file.html</response_content> <response_content>file:///absolute_path_file.html</response_content>
</handler> </handler>
</rule> </rule>
@ -599,6 +631,9 @@ $ curl -v -H 'XXX:xxx' 'http://localhost:8123/get_config_static_handler'
<handler> <handler>
<type>static</type> <type>static</type>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<http_response_headers>
<ETag>737060cd8c284d8af7ad3082f209582d</ETag>
</http_response_headers>
<response_content>file://./relative_path_file.html</response_content> <response_content>file://./relative_path_file.html</response_content>
</handler> </handler>
</rule> </rule>

View File

@ -1609,6 +1609,10 @@ try
0, // We don't need any threads one all the parts will be deleted 0, // We don't need any threads one all the parts will be deleted
new_server_settings.max_parts_cleaning_thread_pool_size); new_server_settings.max_parts_cleaning_thread_pool_size);
global_context->setMergeWorkload(new_server_settings.merge_workload);
global_context->setMutationWorkload(new_server_settings.mutation_workload);
if (config->has("resources")) if (config->has("resources"))
{ {
global_context->getResourceManager()->updateConfiguration(*config); global_context->getResourceManager()->updateConfiguration(*config);

View File

@ -371,7 +371,7 @@
<!-- Enables asynchronous loading of databases and tables to speedup server startup. <!-- Enables asynchronous loading of databases and tables to speedup server startup.
Queries to not yet loaded entity will be blocked until load is finished. Queries to not yet loaded entity will be blocked until load is finished.
--> -->
<!-- <async_load_databases>true</async_load_databases> --> <async_load_databases>true</async_load_databases>
<!-- On memory constrained environments you may have to set this to value larger than 1. <!-- On memory constrained environments you may have to set this to value larger than 1.
--> -->
@ -1396,6 +1396,14 @@
<!-- <host_name>replica</host_name> --> <!-- <host_name>replica</host_name> -->
</distributed_ddl> </distributed_ddl>
<!-- Used to regulate how resources are utilized and shared between merges, mutations and other workloads.
Specified value is used as `workload` setting value for background merge or mutation.
-->
<!--
<merge_workload>merges_and_mutations</merge_workload>
<mutation_workload>merges_and_mutations</mutation_workload>
-->
<!-- Settings to fine-tune MergeTree tables. See documentation in source code, in MergeTreeSettings.h --> <!-- Settings to fine-tune MergeTree tables. See documentation in source code, in MergeTreeSettings.h -->
<!-- <!--
<merge_tree> <merge_tree>

View File

@ -31,6 +31,7 @@ namespace DB
{ {
namespace ErrorCodes namespace ErrorCodes
{ {
extern const int AUTHENTICATION_FAILED;
extern const int SUPPORT_IS_DISABLED; extern const int SUPPORT_IS_DISABLED;
extern const int BAD_ARGUMENTS; extern const int BAD_ARGUMENTS;
extern const int LOGICAL_ERROR; extern const int LOGICAL_ERROR;
@ -90,8 +91,10 @@ bool AuthenticationData::Util::checkPasswordBcrypt(std::string_view password [[m
{ {
#if USE_BCRYPT #if USE_BCRYPT
int ret = bcrypt_checkpw(password.data(), reinterpret_cast<const char *>(password_bcrypt.data())); int ret = bcrypt_checkpw(password.data(), reinterpret_cast<const char *>(password_bcrypt.data()));
/// Before 24.6 we didn't validate hashes on creation, so it could be that the stored hash is invalid
/// and it could not be decoded by the library
if (ret == -1) if (ret == -1)
throw Exception(ErrorCodes::LOGICAL_ERROR, "BCrypt library failed: bcrypt_checkpw returned {}", ret); throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Internal failure decoding Bcrypt hash");
return (ret == 0); return (ret == 0);
#else #else
throw Exception( throw Exception(
@ -230,6 +233,17 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash)
throw Exception(ErrorCodes::BAD_ARGUMENTS, throw Exception(ErrorCodes::BAD_ARGUMENTS,
"Password hash for the 'BCRYPT_PASSWORD' authentication type has length {} " "Password hash for the 'BCRYPT_PASSWORD' authentication type has length {} "
"but must be 59 or 60 bytes.", hash.size()); "but must be 59 or 60 bytes.", hash.size());
auto resized = hash;
resized.resize(64);
#if USE_BCRYPT
/// Verify that it is a valid hash
int ret = bcrypt_checkpw("", reinterpret_cast<const char *>(resized.data()));
if (ret == -1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Could not decode the provided hash with 'bcrypt_hash'");
#endif
password_hash = hash; password_hash = hash;
password_hash.resize(64); password_hash.resize(64);
return; return;

View File

@ -0,0 +1,265 @@
#include <AggregateFunctions/IAggregateFunction.h>
#include <AggregateFunctions/AggregateFunctionFactory.h>
#include <AggregateFunctions/FactoryHelpers.h>
#include <Columns/IColumn.h>
#include <Columns/ColumnNullable.h>
#include <Columns/ColumnString.h>
#include <Core/ServerSettings.h>
#include <Core/ColumnWithTypeAndName.h>
#include <Common/ArenaAllocator.h>
#include <Common/assert_cast.h>
#include <Interpreters/castColumn.h>
#include <DataTypes/IDataType.h>
#include <DataTypes/DataTypeArray.h>
#include <DataTypes/DataTypeString.h>
#include <DataTypes/DataTypesNumber.h>
#include <IO/ReadHelpers.h>
#include <IO/WriteHelpers.h>
namespace DB
{
struct Settings;
namespace ErrorCodes
{
extern const int TOO_MANY_ARGUMENTS_FOR_FUNCTION;
extern const int ILLEGAL_TYPE_OF_ARGUMENT;
extern const int BAD_ARGUMENTS;
}
namespace
{
struct GroupConcatDataBase
{
UInt64 data_size = 0;
UInt64 allocated_size = 0;
char * data = nullptr;
void checkAndUpdateSize(UInt64 add, Arena * arena)
{
if (data_size + add >= allocated_size)
{
auto old_size = allocated_size;
allocated_size = std::max(2 * allocated_size, data_size + add);
data = arena->realloc(data, old_size, allocated_size);
}
}
void insertChar(const char * str, UInt64 str_size, Arena * arena)
{
checkAndUpdateSize(str_size, arena);
memcpy(data + data_size, str, str_size);
data_size += str_size;
}
};
struct GroupConcatData : public GroupConcatDataBase
{
using Offset = UInt64;
using Allocator = MixedAlignedArenaAllocator<alignof(Offset), 4096>;
using Offsets = PODArray<Offset, 32, Allocator>;
/// offset[i * 2] - beginning of the i-th row, offset[i * 2 + 1] - end of the i-th row
Offsets offsets;
UInt64 num_rows = 0;
UInt64 getSize(size_t i) const { return offsets[i * 2 + 1] - offsets[i * 2]; }
UInt64 getString(size_t i) const { return offsets[i * 2]; }
void insert(const IColumn * column, const SerializationPtr & serialization, size_t row_num, Arena * arena)
{
WriteBufferFromOwnString buff;
serialization->serializeText(*column, row_num, buff, {});
auto string = buff.stringView();
checkAndUpdateSize(string.size(), arena);
memcpy(data + data_size, string.data(), string.size());
offsets.push_back(data_size, arena);
data_size += string.size();
offsets.push_back(data_size, arena);
num_rows++;
}
};
template <bool has_limit>
class GroupConcatImpl final
: public IAggregateFunctionDataHelper<GroupConcatData, GroupConcatImpl<has_limit>>
{
static constexpr auto name = "groupConcat";
SerializationPtr serialization;
UInt64 limit;
const String delimiter;
public:
GroupConcatImpl(const DataTypePtr & data_type_, const Array & parameters_, UInt64 limit_, const String & delimiter_)
: IAggregateFunctionDataHelper<GroupConcatData, GroupConcatImpl<has_limit>>(
{data_type_}, parameters_, std::make_shared<DataTypeString>())
, serialization(this->argument_types[0]->getDefaultSerialization())
, limit(limit_)
, delimiter(delimiter_)
{
}
String getName() const override { return name; }
void add(AggregateDataPtr __restrict place, const IColumn ** columns, size_t row_num, Arena * arena) const override
{
auto & cur_data = this->data(place);
if constexpr (has_limit)
if (cur_data.num_rows >= limit)
return;
if (cur_data.data_size != 0)
cur_data.insertChar(delimiter.c_str(), delimiter.size(), arena);
cur_data.insert(columns[0], serialization, row_num, arena);
}
void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs, Arena * arena) const override
{
auto & cur_data = this->data(place);
auto & rhs_data = this->data(rhs);
if (rhs_data.data_size == 0)
return;
if constexpr (has_limit)
{
UInt64 new_elems_count = std::min(rhs_data.num_rows, limit - cur_data.num_rows);
for (UInt64 i = 0; i < new_elems_count; ++i)
{
if (cur_data.data_size != 0)
cur_data.insertChar(delimiter.c_str(), delimiter.size(), arena);
cur_data.offsets.push_back(cur_data.data_size, arena);
cur_data.insertChar(rhs_data.data + rhs_data.getString(i), rhs_data.getSize(i), arena);
cur_data.num_rows++;
cur_data.offsets.push_back(cur_data.data_size, arena);
}
}
else
{
if (cur_data.data_size != 0)
cur_data.insertChar(delimiter.c_str(), delimiter.size(), arena);
cur_data.insertChar(rhs_data.data, rhs_data.data_size, arena);
}
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> /* version */) const override
{
auto & cur_data = this->data(place);
writeVarUInt(cur_data.data_size, buf);
writeVarUInt(cur_data.allocated_size, buf);
buf.write(cur_data.data, cur_data.data_size);
if constexpr (has_limit)
{
writeVarUInt(cur_data.num_rows, buf);
for (const auto & offset : cur_data.offsets)
writeVarUInt(offset, buf);
}
}
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> /* version */, Arena * arena) const override
{
auto & cur_data = this->data(place);
readVarUInt(cur_data.data_size, buf);
readVarUInt(cur_data.allocated_size, buf);
buf.readStrict(cur_data.data, cur_data.data_size);
if constexpr (has_limit)
{
readVarUInt(cur_data.num_rows, buf);
cur_data.offsets.resize_exact(cur_data.num_rows * 2, arena);
for (auto & offset : cur_data.offsets)
readVarUInt(offset, buf);
}
}
void insertResultInto(AggregateDataPtr __restrict place, IColumn & to, Arena *) const override
{
auto & cur_data = this->data(place);
if (cur_data.data_size == 0)
{
auto column_nullable = IColumn::mutate(makeNullable(to.getPtr()));
column_nullable->insertDefault();
return;
}
auto & column_string = assert_cast<ColumnString &>(to);
column_string.insertData(cur_data.data, cur_data.data_size);
}
bool allocatesMemoryInArena() const override { return true; }
};
AggregateFunctionPtr createAggregateFunctionGroupConcat(
const std::string & name, const DataTypes & argument_types, const Array & parameters, const Settings *)
{
assertUnary(name, argument_types);
bool has_limit = false;
UInt64 limit = 0;
String delimiter;
if (parameters.size() > 2)
throw Exception(ErrorCodes::TOO_MANY_ARGUMENTS_FOR_FUNCTION,
"Incorrect number of parameters for aggregate function {}, should be 0, 1 or 2, got: {}", name, parameters.size());
if (!parameters.empty())
{
auto type = parameters[0].getType();
if (type != Field::Types::String)
throw Exception(ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT, "First parameter for aggregate function {} should be string", name);
delimiter = parameters[0].get<String>();
}
if (parameters.size() == 2)
{
auto type = parameters[1].getType();
if (type != Field::Types::Int64 && type != Field::Types::UInt64)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Second parameter for aggregate function {} should be a positive number", name);
if ((type == Field::Types::Int64 && parameters[1].get<Int64>() <= 0) ||
(type == Field::Types::UInt64 && parameters[1].get<UInt64>() == 0))
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Second parameter for aggregate function {} should be a positive number, got: {}", name, parameters[1].get<Int64>());
has_limit = true;
limit = parameters[1].get<UInt64>();
}
if (has_limit)
return std::make_shared<GroupConcatImpl</* has_limit= */ true>>(argument_types[0], parameters, limit, delimiter);
else
return std::make_shared<GroupConcatImpl</* has_limit= */ false>>(argument_types[0], parameters, limit, delimiter);
}
}
void registerAggregateFunctionGroupConcat(AggregateFunctionFactory & factory)
{
AggregateFunctionProperties properties = { .returns_default_when_only_null = false, .is_order_dependent = true };
factory.registerFunction("groupConcat", { createAggregateFunctionGroupConcat, properties });
factory.registerAlias("group_concat", "groupConcat", AggregateFunctionFactory::CaseInsensitive);
}
}

View File

@ -19,6 +19,7 @@ void registerAggregateFunctionGroupArraySorted(AggregateFunctionFactory & factor
void registerAggregateFunctionGroupUniqArray(AggregateFunctionFactory &); void registerAggregateFunctionGroupUniqArray(AggregateFunctionFactory &);
void registerAggregateFunctionGroupArrayInsertAt(AggregateFunctionFactory &); void registerAggregateFunctionGroupArrayInsertAt(AggregateFunctionFactory &);
void registerAggregateFunctionGroupArrayIntersect(AggregateFunctionFactory &); void registerAggregateFunctionGroupArrayIntersect(AggregateFunctionFactory &);
void registerAggregateFunctionGroupConcat(AggregateFunctionFactory &);
void registerAggregateFunctionsQuantile(AggregateFunctionFactory &); void registerAggregateFunctionsQuantile(AggregateFunctionFactory &);
void registerAggregateFunctionsQuantileDeterministic(AggregateFunctionFactory &); void registerAggregateFunctionsQuantileDeterministic(AggregateFunctionFactory &);
void registerAggregateFunctionsQuantileExact(AggregateFunctionFactory &); void registerAggregateFunctionsQuantileExact(AggregateFunctionFactory &);
@ -120,6 +121,7 @@ void registerAggregateFunctions()
registerAggregateFunctionGroupUniqArray(factory); registerAggregateFunctionGroupUniqArray(factory);
registerAggregateFunctionGroupArrayInsertAt(factory); registerAggregateFunctionGroupArrayInsertAt(factory);
registerAggregateFunctionGroupArrayIntersect(factory); registerAggregateFunctionGroupArrayIntersect(factory);
registerAggregateFunctionGroupConcat(factory);
registerAggregateFunctionsQuantile(factory); registerAggregateFunctionsQuantile(factory);
registerAggregateFunctionsQuantileDeterministic(factory); registerAggregateFunctionsQuantileDeterministic(factory);
registerAggregateFunctionsQuantileExact(factory); registerAggregateFunctionsQuantileExact(factory);

View File

@ -985,18 +985,18 @@ std::string QueryAnalyzer::rewriteAggregateFunctionNameIfNeeded(
{ {
result_aggregate_function_name = settings.count_distinct_implementation; result_aggregate_function_name = settings.count_distinct_implementation;
} }
else if (aggregate_function_name_lowercase == "countdistinctif" || aggregate_function_name_lowercase == "countifdistinct") else if (aggregate_function_name_lowercase == "countifdistinct" ||
(settings.rewrite_count_distinct_if_with_count_distinct_implementation && aggregate_function_name_lowercase == "countdistinctif"))
{ {
result_aggregate_function_name = settings.count_distinct_implementation; result_aggregate_function_name = settings.count_distinct_implementation;
result_aggregate_function_name += "If"; result_aggregate_function_name += "If";
} }
else if (aggregate_function_name_lowercase.ends_with("ifdistinct"))
/// Replace aggregateFunctionIfDistinct into aggregateFunctionDistinctIf to make execution more optimal
if (result_aggregate_function_name.ends_with("ifdistinct"))
{ {
/// Replace aggregateFunctionIfDistinct into aggregateFunctionDistinctIf to make execution more optimal
size_t prefix_length = result_aggregate_function_name.size() - strlen("ifdistinct"); size_t prefix_length = result_aggregate_function_name.size() - strlen("ifdistinct");
result_aggregate_function_name = result_aggregate_function_name.substr(0, prefix_length) + "DistinctIf"; result_aggregate_function_name = result_aggregate_function_name.substr(0, prefix_length) + "DistinctIf";
} }
bool need_add_or_null = settings.aggregate_functions_null_for_empty && !result_aggregate_function_name.ends_with("OrNull"); bool need_add_or_null = settings.aggregate_functions_null_for_empty && !result_aggregate_function_name.ends_with("OrNull");
if (need_add_or_null) if (need_add_or_null)

View File

@ -85,9 +85,18 @@ StatusFile::StatusFile(std::string path_, FillFunction fill_)
/// Write information about current server instance to the file. /// Write information about current server instance to the file.
WriteBufferFromFileDescriptor out(fd, 1024); WriteBufferFromFileDescriptor out(fd, 1024);
fill(out); try
/// Finalize here to avoid throwing exceptions in destructor. {
out.finalize(); fill(out);
/// Finalize here to avoid throwing exceptions in destructor.
out.finalize();
}
catch (...)
{
/// Finalize in case of exception to avoid throwing exceptions in destructor
out.finalize();
throw;
}
} }
catch (...) catch (...)
{ {

View File

@ -609,7 +609,10 @@ void KeeperStorage::UncommittedState::commit(int64_t commit_zxid)
uncommitted_auth.pop_front(); uncommitted_auth.pop_front();
if (uncommitted_auth.empty()) if (uncommitted_auth.empty())
session_and_auth.erase(add_auth->session_id); session_and_auth.erase(add_auth->session_id);
}
else if (auto * close_session = std::get_if<CloseSessionDelta>(&front_delta.operation))
{
closed_sessions.erase(close_session->session_id);
} }
deltas.pop_front(); deltas.pop_front();
@ -682,6 +685,10 @@ void KeeperStorage::UncommittedState::rollback(int64_t rollback_zxid)
session_and_auth.erase(add_auth->session_id); session_and_auth.erase(add_auth->session_id);
} }
} }
else if (auto * close_session = std::get_if<CloseSessionDelta>(&delta_it->operation))
{
closed_sessions.erase(close_session->session_id);
}
} }
if (delta_it == deltas.rend()) if (delta_it == deltas.rend())
@ -878,6 +885,10 @@ Coordination::Error KeeperStorage::commit(int64_t commit_zxid)
session_and_auth[operation.session_id].emplace_back(std::move(operation.auth_id)); session_and_auth[operation.session_id].emplace_back(std::move(operation.auth_id));
return Coordination::Error::ZOK; return Coordination::Error::ZOK;
} }
else if constexpr (std::same_as<DeltaType, KeeperStorage::CloseSessionDelta>)
{
return Coordination::Error::ZOK;
}
else else
{ {
// shouldn't be called in any process functions // shouldn't be called in any process functions
@ -2366,12 +2377,15 @@ void KeeperStorage::preprocessRequest(
ephemerals.erase(session_ephemerals); ephemerals.erase(session_ephemerals);
} }
new_deltas.emplace_back(transaction.zxid, CloseSessionDelta{session_id});
uncommitted_state.closed_sessions.insert(session_id);
new_digest = calculateNodesDigest(new_digest, new_deltas); new_digest = calculateNodesDigest(new_digest, new_deltas);
return; return;
} }
if (check_acl && !request_processor->checkAuth(*this, session_id, false)) if ((check_acl && !request_processor->checkAuth(*this, session_id, false)) ||
uncommitted_state.closed_sessions.contains(session_id)) // Is session closed but not committed yet
{ {
uncommitted_state.deltas.emplace_back(new_last_zxid, Coordination::Error::ZNOAUTH); uncommitted_state.deltas.emplace_back(new_last_zxid, Coordination::Error::ZNOAUTH);
return; return;

View File

@ -314,8 +314,13 @@ public:
AuthID auth_id; AuthID auth_id;
}; };
struct CloseSessionDelta
{
int64_t session_id;
};
using Operation = std:: using Operation = std::
variant<CreateNodeDelta, RemoveNodeDelta, UpdateNodeDelta, SetACLDelta, AddAuthDelta, ErrorDelta, SubDeltaEnd, FailedMultiDelta>; variant<CreateNodeDelta, RemoveNodeDelta, UpdateNodeDelta, SetACLDelta, AddAuthDelta, ErrorDelta, SubDeltaEnd, FailedMultiDelta, CloseSessionDelta>;
struct Delta struct Delta
{ {
@ -351,6 +356,7 @@ public:
std::shared_ptr<Node> tryGetNodeFromStorage(StringRef path) const; std::shared_ptr<Node> tryGetNodeFromStorage(StringRef path) const;
std::unordered_map<int64_t, std::list<const AuthID *>> session_and_auth; std::unordered_map<int64_t, std::list<const AuthID *>> session_and_auth;
std::unordered_set<int64_t> closed_sessions;
struct UncommittedNode struct UncommittedNode
{ {

View File

@ -2019,6 +2019,67 @@ TEST_P(CoordinationTest, TestCreateNodeWithAuthSchemeForAclWhenAuthIsPrecommitte
EXPECT_EQ(acls[0].permissions, 31); EXPECT_EQ(acls[0].permissions, 31);
} }
TEST_P(CoordinationTest, TestPreprocessWhenCloseSessionIsPrecommitted)
{
using namespace Coordination;
using namespace DB;
ChangelogDirTest snapshots("./snapshots");
setSnapshotDirectory("./snapshots");
ResponsesQueue queue(std::numeric_limits<size_t>::max());
SnapshotsQueue snapshots_queue{1};
int64_t session_id = 1;
size_t term = 0;
auto state_machine = std::make_shared<KeeperStateMachine>(queue, snapshots_queue, keeper_context, nullptr);
state_machine->init();
auto & storage = state_machine->getStorageUnsafe();
const auto & uncommitted_state = storage.uncommitted_state;
// Create first node for the session
String node_path_1 = "/node_1";
std::shared_ptr<ZooKeeperCreateRequest> create_req_1 = std::make_shared<ZooKeeperCreateRequest>();
create_req_1->path = node_path_1;
auto create_entry_1 = getLogEntryFromZKRequest(term, session_id, state_machine->getNextZxid(), create_req_1);
state_machine->pre_commit(1, create_entry_1->get_buf());
EXPECT_TRUE(uncommitted_state.nodes.contains(node_path_1));
state_machine->commit(1, create_entry_1->get_buf());
EXPECT_TRUE(storage.container.contains(node_path_1));
// Close session
std::shared_ptr<ZooKeeperCloseRequest> close_req = std::make_shared<ZooKeeperCloseRequest>();
auto close_entry = getLogEntryFromZKRequest(term, session_id, state_machine->getNextZxid(), close_req);
// Pre-commit close session
state_machine->pre_commit(2, close_entry->get_buf());
// Try to create second node after close session is pre-committed
String node_path_2 = "/node_2";
std::shared_ptr<ZooKeeperCreateRequest> create_req_2 = std::make_shared<ZooKeeperCreateRequest>();
create_req_2->path = node_path_2;
auto create_entry_2 = getLogEntryFromZKRequest(term, session_id, state_machine->getNextZxid(), create_req_2);
// Pre-commit creating second node
state_machine->pre_commit(3, create_entry_2->get_buf());
// Second node wasn't created
EXPECT_FALSE(uncommitted_state.nodes.contains(node_path_2));
// Rollback pre-committed closing session
state_machine->rollback(3, create_entry_2->get_buf());
state_machine->rollback(2, close_entry->get_buf());
// Pre-commit creating second node
state_machine->pre_commit(2, create_entry_2->get_buf());
// Now second node was created
EXPECT_TRUE(uncommitted_state.nodes.contains(node_path_2));
state_machine->commit(2, create_entry_2->get_buf());
EXPECT_TRUE(storage.container.contains(node_path_1));
EXPECT_TRUE(storage.container.contains(node_path_2));
}
TEST_P(CoordinationTest, TestSetACLWithAuthSchemeForAclWhenAuthIsPrecommitted) TEST_P(CoordinationTest, TestSetACLWithAuthSchemeForAclWhenAuthIsPrecommitted)
{ {
using namespace Coordination; using namespace Coordination;

View File

@ -146,6 +146,8 @@ namespace DB
M(UInt64, global_profiler_real_time_period_ns, 0, "Period for real clock timer of global profiler (in nanoseconds). Set 0 value to turn off the real clock global profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling.", 0) \ M(UInt64, global_profiler_real_time_period_ns, 0, "Period for real clock timer of global profiler (in nanoseconds). Set 0 value to turn off the real clock global profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling.", 0) \
M(UInt64, global_profiler_cpu_time_period_ns, 0, "Period for CPU clock timer of global profiler (in nanoseconds). Set 0 value to turn off the CPU clock global profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling.", 0) \ M(UInt64, global_profiler_cpu_time_period_ns, 0, "Period for CPU clock timer of global profiler (in nanoseconds). Set 0 value to turn off the CPU clock global profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling.", 0) \
M(Bool, enable_azure_sdk_logging, false, "Enables logging from Azure sdk", 0) \ M(Bool, enable_azure_sdk_logging, false, "Enables logging from Azure sdk", 0) \
M(String, merge_workload, "default", "Name of workload to be used to access resources for all merges (may be overridden by a merge tree setting)", 0) \
M(String, mutation_workload, "default", "Name of workload to be used to access resources for all mutations (may be overridden by a merge tree setting)", 0) \
M(Double, gwp_asan_force_sample_probability, 0, "Probability that an allocation from specific places will be sampled by GWP Asan (i.e. PODArray allocations)", 0) \ M(Double, gwp_asan_force_sample_probability, 0, "Probability that an allocation from specific places will be sampled by GWP Asan (i.e. PODArray allocations)", 0) \
/// If you add a setting which can be updated at runtime, please update 'changeable_settings' map in StorageSystemServerSettings.cpp /// If you add a setting which can be updated at runtime, please update 'changeable_settings' map in StorageSystemServerSettings.cpp

View File

@ -1144,7 +1144,9 @@ class IColumn;
M(UInt64, output_format_pretty_max_value_width, 10000, "Maximum width of value to display in Pretty formats. If greater - it will be cut.", 0) \ M(UInt64, output_format_pretty_max_value_width, 10000, "Maximum width of value to display in Pretty formats. If greater - it will be cut.", 0) \
M(UInt64, output_format_pretty_max_value_width_apply_for_single_value, false, "Only cut values (see the `output_format_pretty_max_value_width` setting) when it is not a single value in a block. Otherwise output it entirely, which is useful for the `SHOW CREATE TABLE` query.", 0) \ M(UInt64, output_format_pretty_max_value_width_apply_for_single_value, false, "Only cut values (see the `output_format_pretty_max_value_width` setting) when it is not a single value in a block. Otherwise output it entirely, which is useful for the `SHOW CREATE TABLE` query.", 0) \
M(UInt64Auto, output_format_pretty_color, "auto", "Use ANSI escape sequences in Pretty formats. 0 - disabled, 1 - enabled, 'auto' - enabled if a terminal.", 0) \ M(UInt64Auto, output_format_pretty_color, "auto", "Use ANSI escape sequences in Pretty formats. 0 - disabled, 1 - enabled, 'auto' - enabled if a terminal.", 0) \
M(String, output_format_pretty_grid_charset, "UTF-8", "Charset for printing grid borders. Available charsets: ASCII, UTF-8 (default one).", 0) \ M(String, output_format_pretty_grid_charset, "UTF-8", "Charset for printing grid borders. Available charsets: ASCII, UTF-8 (default one).", 0) \
M(UInt64, output_format_pretty_display_footer_column_names, true, "Display column names in the footer if there are 999 or more rows.", 0) \
M(UInt64, output_format_pretty_display_footer_column_names_min_rows, 50, "Sets the minimum threshold value of rows for which to enable displaying column names in the footer. 50 (default)", 0) \
M(UInt64, output_format_parquet_row_group_size, 1000000, "Target row group size in rows.", 0) \ M(UInt64, output_format_parquet_row_group_size, 1000000, "Target row group size in rows.", 0) \
M(UInt64, output_format_parquet_row_group_size_bytes, 512 * 1024 * 1024, "Target row group size in bytes, before compression.", 0) \ M(UInt64, output_format_parquet_row_group_size_bytes, 512 * 1024 * 1024, "Target row group size in bytes, before compression.", 0) \
M(Bool, output_format_parquet_string_as_string, true, "Use Parquet String type instead of Binary for String columns.", 0) \ M(Bool, output_format_parquet_string_as_string, true, "Use Parquet String type instead of Binary for String columns.", 0) \

View File

@ -108,6 +108,8 @@ static const std::map<ClickHouseVersion, SettingsChangesHistory::SettingsChanges
{"enable_vertical_final", false, true, "Enable vertical final by default again after fixing bug"}, {"enable_vertical_final", false, true, "Enable vertical final by default again after fixing bug"},
{"parallel_replicas_custom_key_range_lower", 0, 0, "Add settings to control the range filter when using parallel replicas with dynamic shards"}, {"parallel_replicas_custom_key_range_lower", 0, 0, "Add settings to control the range filter when using parallel replicas with dynamic shards"},
{"parallel_replicas_custom_key_range_upper", 0, 0, "Add settings to control the range filter when using parallel replicas with dynamic shards. A value of 0 disables the upper limit"}, {"parallel_replicas_custom_key_range_upper", 0, 0, "Add settings to control the range filter when using parallel replicas with dynamic shards. A value of 0 disables the upper limit"},
{"output_format_pretty_display_footer_column_names", 0, 1, "Add a setting to display column names in the footer if there are many rows. Threshold value is controlled by output_format_pretty_display_footer_column_names_min_rows."},
{"output_format_pretty_display_footer_column_names_min_rows", 0, 50, "Add a setting to control the threshold value for setting output_format_pretty_display_footer_column_names_min_rows. Default 50."},
{"output_format_csv_serialize_tuple_into_separate_columns", true, true, "A new way of how interpret tuples in CSV format was added."}, {"output_format_csv_serialize_tuple_into_separate_columns", true, true, "A new way of how interpret tuples in CSV format was added."},
{"input_format_csv_deserialize_separate_columns_into_tuple", true, true, "A new way of how interpret tuples in CSV format was added."}, {"input_format_csv_deserialize_separate_columns_into_tuple", true, true, "A new way of how interpret tuples in CSV format was added."},
{"input_format_csv_try_infer_strings_from_quoted_tuples", true, true, "A new way of how interpret tuples in CSV format was added."}, {"input_format_csv_try_infer_strings_from_quoted_tuples", true, true, "A new way of how interpret tuples in CSV format was added."},

View File

@ -146,7 +146,7 @@ void SerializationVariantElement::deserializeBinaryBulkWithMultipleStreams(
} }
/// If we started to read a new column, reinitialize variant column in deserialization state. /// If we started to read a new column, reinitialize variant column in deserialization state.
if (!variant_element_state->variant || result_column->empty()) if (!variant_element_state->variant || mutable_column->empty())
{ {
variant_element_state->variant = mutable_column->cloneEmpty(); variant_element_state->variant = mutable_column->cloneEmpty();

View File

@ -175,8 +175,7 @@ Columns DirectDictionary<dictionary_key_type>::getColumns(
if (!mask_filled) if (!mask_filled)
(*default_mask)[requested_key_index] = 1; (*default_mask)[requested_key_index] = 1;
Field value{}; result_column->insertDefault();
result_column->insert(value);
} }
else else
{ {

View File

@ -181,6 +181,8 @@ FormatSettings getFormatSettings(const ContextPtr & context, const Settings & se
format_settings.pretty.highlight_digit_groups = settings.output_format_pretty_highlight_digit_groups; format_settings.pretty.highlight_digit_groups = settings.output_format_pretty_highlight_digit_groups;
format_settings.pretty.output_format_pretty_row_numbers = settings.output_format_pretty_row_numbers; format_settings.pretty.output_format_pretty_row_numbers = settings.output_format_pretty_row_numbers;
format_settings.pretty.output_format_pretty_single_large_number_tip_threshold = settings.output_format_pretty_single_large_number_tip_threshold; format_settings.pretty.output_format_pretty_single_large_number_tip_threshold = settings.output_format_pretty_single_large_number_tip_threshold;
format_settings.pretty.output_format_pretty_display_footer_column_names = settings.output_format_pretty_display_footer_column_names;
format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows = settings.output_format_pretty_display_footer_column_names_min_rows;
format_settings.protobuf.input_flatten_google_wrappers = settings.input_format_protobuf_flatten_google_wrappers; format_settings.protobuf.input_flatten_google_wrappers = settings.input_format_protobuf_flatten_google_wrappers;
format_settings.protobuf.output_nullables_with_google_wrappers = settings.output_format_protobuf_nullables_with_google_wrappers; format_settings.protobuf.output_nullables_with_google_wrappers = settings.output_format_protobuf_nullables_with_google_wrappers;
format_settings.protobuf.skip_fields_with_unsupported_types_in_schema_inference = settings.input_format_protobuf_skip_fields_with_unsupported_types_in_schema_inference; format_settings.protobuf.skip_fields_with_unsupported_types_in_schema_inference = settings.input_format_protobuf_skip_fields_with_unsupported_types_in_schema_inference;

View File

@ -289,6 +289,8 @@ struct FormatSettings
bool output_format_pretty_row_numbers = false; bool output_format_pretty_row_numbers = false;
UInt64 output_format_pretty_single_large_number_tip_threshold = 1'000'000; UInt64 output_format_pretty_single_large_number_tip_threshold = 1'000'000;
UInt64 output_format_pretty_display_footer_column_names = 1;
UInt64 output_format_pretty_display_footer_column_names_min_rows = 50;
enum class Charset : uint8_t enum class Charset : uint8_t
{ {

View File

@ -77,7 +77,15 @@ WriteBufferFromFile::~WriteBufferFromFile()
if (fd < 0) if (fd < 0)
return; return;
finalize(); try
{
finalize();
}
catch (...)
{
tryLogCurrentException(__PRETTY_FUNCTION__);
}
int err = ::close(fd); int err = ::close(fd);
/// Everything except for EBADF should be ignored in dtor, since all of /// Everything except for EBADF should be ignored in dtor, since all of
/// others (EINTR/EIO/ENOSPC/EDQUOT) could be possible during writing to /// others (EINTR/EIO/ENOSPC/EDQUOT) could be possible during writing to

View File

@ -105,7 +105,14 @@ WriteBufferFromFileDescriptor::WriteBufferFromFileDescriptor(
WriteBufferFromFileDescriptor::~WriteBufferFromFileDescriptor() WriteBufferFromFileDescriptor::~WriteBufferFromFileDescriptor()
{ {
finalize(); try
{
finalize();
}
catch (...)
{
tryLogCurrentException(__PRETTY_FUNCTION__);
}
} }
void WriteBufferFromFileDescriptor::finalizeImpl() void WriteBufferFromFileDescriptor::finalizeImpl()

View File

@ -281,6 +281,8 @@ struct ContextSharedPart : boost::noncopyable
String default_profile_name; /// Default profile name used for default values. String default_profile_name; /// Default profile name used for default values.
String system_profile_name; /// Profile used by system processes String system_profile_name; /// Profile used by system processes
String buffer_profile_name; /// Profile used by Buffer engine for flushing to the underlying String buffer_profile_name; /// Profile used by Buffer engine for flushing to the underlying
String merge_workload TSA_GUARDED_BY(mutex); /// Workload setting value that is used by all merges
String mutation_workload TSA_GUARDED_BY(mutex); /// Workload setting value that is used by all mutations
std::unique_ptr<AccessControl> access_control TSA_GUARDED_BY(mutex); std::unique_ptr<AccessControl> access_control TSA_GUARDED_BY(mutex);
mutable OnceFlag resource_manager_initialized; mutable OnceFlag resource_manager_initialized;
mutable ResourceManagerPtr resource_manager; mutable ResourceManagerPtr resource_manager;
@ -1561,11 +1563,36 @@ ResourceManagerPtr Context::getResourceManager() const
ClassifierPtr Context::getWorkloadClassifier() const ClassifierPtr Context::getWorkloadClassifier() const
{ {
std::lock_guard lock(mutex); std::lock_guard lock(mutex);
// NOTE: Workload cannot be changed after query start, and getWorkloadClassifier() should not be called before proper `workload` is set
if (!classifier) if (!classifier)
classifier = getResourceManager()->acquire(getSettingsRef().workload); classifier = getResourceManager()->acquire(getSettingsRef().workload);
return classifier; return classifier;
} }
String Context::getMergeWorkload() const
{
SharedLockGuard lock(shared->mutex);
return shared->merge_workload;
}
void Context::setMergeWorkload(const String & value)
{
std::lock_guard lock(shared->mutex);
shared->merge_workload = value;
}
String Context::getMutationWorkload() const
{
SharedLockGuard lock(shared->mutex);
return shared->mutation_workload;
}
void Context::setMutationWorkload(const String & value)
{
std::lock_guard lock(shared->mutex);
shared->mutation_workload = value;
}
Scalars Context::getScalars() const Scalars Context::getScalars() const
{ {
@ -2513,6 +2540,20 @@ void Context::makeQueryContext()
backups_query_throttler.reset(); backups_query_throttler.reset();
} }
void Context::makeQueryContextForMerge(const MergeTreeSettings & merge_tree_settings)
{
makeQueryContext();
classifier.reset(); // It is assumed that there are no active queries running using this classifier, otherwise this will lead to crashes
settings.workload = merge_tree_settings.merge_workload.value.empty() ? getMergeWorkload() : merge_tree_settings.merge_workload;
}
void Context::makeQueryContextForMutate(const MergeTreeSettings & merge_tree_settings)
{
makeQueryContext();
classifier.reset(); // It is assumed that there are no active queries running using this classifier, otherwise this will lead to crashes
settings.workload = merge_tree_settings.mutation_workload.value.empty() ? getMutationWorkload() : merge_tree_settings.mutation_workload;
}
void Context::makeSessionContext() void Context::makeSessionContext()
{ {
session_context = shared_from_this(); session_context = shared_from_this();

View File

@ -622,6 +622,10 @@ public:
/// Resource management related /// Resource management related
ResourceManagerPtr getResourceManager() const; ResourceManagerPtr getResourceManager() const;
ClassifierPtr getWorkloadClassifier() const; ClassifierPtr getWorkloadClassifier() const;
String getMergeWorkload() const;
void setMergeWorkload(const String & value);
String getMutationWorkload() const;
void setMutationWorkload(const String & value);
/// We have to copy external tables inside executeQuery() to track limits. Therefore, set callback for it. Must set once. /// We have to copy external tables inside executeQuery() to track limits. Therefore, set callback for it. Must set once.
void setExternalTablesInitializer(ExternalTablesInitializer && initializer); void setExternalTablesInitializer(ExternalTablesInitializer && initializer);
@ -907,6 +911,8 @@ public:
void setSessionContext(ContextMutablePtr context_) { session_context = context_; } void setSessionContext(ContextMutablePtr context_) { session_context = context_; }
void makeQueryContext(); void makeQueryContext();
void makeQueryContextForMerge(const MergeTreeSettings & merge_tree_settings);
void makeQueryContextForMutate(const MergeTreeSettings & merge_tree_settings);
void makeSessionContext(); void makeSessionContext();
void makeGlobalContext(); void makeGlobalContext();

View File

@ -116,6 +116,12 @@ struct GridSymbols
const char * dash = ""; const char * dash = "";
const char * bold_bar = ""; const char * bold_bar = "";
const char * bar = ""; const char * bar = "";
const char * bold_right_separator_footer = "";
const char * bold_left_separator_footer = "";
const char * bold_middle_separator_footer = "";
const char * bold_left_bottom_corner = "";
const char * bold_right_bottom_corner = "";
const char * bold_bottom_separator = "";
}; };
GridSymbols utf8_grid_symbols; GridSymbols utf8_grid_symbols;
@ -182,47 +188,58 @@ void PrettyBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port_kind
Widths name_widths; Widths name_widths;
calculateWidths(header, chunk, widths, max_widths, name_widths); calculateWidths(header, chunk, widths, max_widths, name_widths);
const GridSymbols & grid_symbols = format_settings.pretty.charset == FormatSettings::Pretty::Charset::UTF8 ? const GridSymbols & grid_symbols
utf8_grid_symbols : = format_settings.pretty.charset == FormatSettings::Pretty::Charset::UTF8 ? utf8_grid_symbols : ascii_grid_symbols;
ascii_grid_symbols;
/// Create separators /// Create separators
WriteBufferFromOwnString top_separator; WriteBufferFromOwnString top_separator;
WriteBufferFromOwnString middle_names_separator; WriteBufferFromOwnString middle_names_separator;
WriteBufferFromOwnString middle_values_separator; WriteBufferFromOwnString middle_values_separator;
WriteBufferFromOwnString bottom_separator; WriteBufferFromOwnString bottom_separator;
WriteBufferFromOwnString footer_top_separator;
WriteBufferFromOwnString footer_bottom_separator;
top_separator << grid_symbols.bold_left_top_corner; top_separator << grid_symbols.bold_left_top_corner;
middle_names_separator << grid_symbols.bold_left_separator; middle_names_separator << grid_symbols.bold_left_separator;
middle_values_separator << grid_symbols.left_separator; middle_values_separator << grid_symbols.left_separator;
bottom_separator << grid_symbols.left_bottom_corner; bottom_separator << grid_symbols.left_bottom_corner;
footer_top_separator << grid_symbols.bold_left_separator_footer;
footer_bottom_separator << grid_symbols.bold_left_bottom_corner;
for (size_t i = 0; i < num_columns; ++i) for (size_t i = 0; i < num_columns; ++i)
{ {
if (i != 0) if (i != 0)
{ {
top_separator << grid_symbols.bold_top_separator; top_separator << grid_symbols.bold_top_separator;
middle_names_separator << grid_symbols.bold_middle_separator; middle_names_separator << grid_symbols.bold_middle_separator;
middle_values_separator << grid_symbols.middle_separator; middle_values_separator << grid_symbols.middle_separator;
bottom_separator << grid_symbols.bottom_separator; bottom_separator << grid_symbols.bottom_separator;
footer_top_separator << grid_symbols.bold_middle_separator_footer;
footer_bottom_separator << grid_symbols.bold_bottom_separator;
} }
for (size_t j = 0; j < max_widths[i] + 2; ++j) for (size_t j = 0; j < max_widths[i] + 2; ++j)
{ {
top_separator << grid_symbols.bold_dash; top_separator << grid_symbols.bold_dash;
middle_names_separator << grid_symbols.bold_dash; middle_names_separator << grid_symbols.bold_dash;
middle_values_separator << grid_symbols.dash; middle_values_separator << grid_symbols.dash;
bottom_separator << grid_symbols.dash; bottom_separator << grid_symbols.dash;
footer_top_separator << grid_symbols.bold_dash;
footer_bottom_separator << grid_symbols.bold_dash;
} }
} }
top_separator << grid_symbols.bold_right_top_corner << "\n"; top_separator << grid_symbols.bold_right_top_corner << "\n";
middle_names_separator << grid_symbols.bold_right_separator << "\n"; middle_names_separator << grid_symbols.bold_right_separator << "\n";
middle_values_separator << grid_symbols.right_separator << "\n"; middle_values_separator << grid_symbols.right_separator << "\n";
bottom_separator << grid_symbols.right_bottom_corner << "\n"; bottom_separator << grid_symbols.right_bottom_corner << "\n";
footer_top_separator << grid_symbols.bold_right_separator_footer << "\n";
footer_bottom_separator << grid_symbols.bold_right_bottom_corner << "\n";
std::string top_separator_s = top_separator.str(); std::string top_separator_s = top_separator.str();
std::string middle_names_separator_s = middle_names_separator.str(); std::string middle_names_separator_s = middle_names_separator.str();
std::string middle_values_separator_s = middle_values_separator.str(); std::string middle_values_separator_s = middle_values_separator.str();
std::string bottom_separator_s = bottom_separator.str(); std::string bottom_separator_s = bottom_separator.str();
std::string footer_top_separator_s = footer_top_separator.str();
std::string footer_bottom_separator_s = footer_bottom_separator.str();
if (format_settings.pretty.output_format_pretty_row_numbers) if (format_settings.pretty.output_format_pretty_row_numbers)
{ {
@ -239,43 +256,47 @@ void PrettyBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port_kind
} }
/// Names /// Names
writeCString(grid_symbols.bold_bar, out); auto write_names = [&]() -> void
writeCString(" ", out);
for (size_t i = 0; i < num_columns; ++i)
{ {
if (i != 0) writeCString(grid_symbols.bold_bar, out);
writeCString(" ", out);
for (size_t i = 0; i < num_columns; ++i)
{ {
writeCString(" ", out); if (i != 0)
writeCString(grid_symbols.bold_bar, out); {
writeCString(" ", out); writeCString(" ", out);
writeCString(grid_symbols.bold_bar, out);
writeCString(" ", out);
}
const auto & col = header.getByPosition(i);
if (color)
writeCString("\033[1m", out);
if (col.type->shouldAlignRightInPrettyFormats())
{
for (size_t k = 0; k < max_widths[i] - name_widths[i]; ++k)
writeChar(' ', out);
writeString(col.name, out);
}
else
{
writeString(col.name, out);
for (size_t k = 0; k < max_widths[i] - name_widths[i]; ++k)
writeChar(' ', out);
}
if (color)
writeCString("\033[0m", out);
} }
writeCString(" ", out);
const auto & col = header.getByPosition(i); writeCString(grid_symbols.bold_bar, out);
writeCString("\n", out);
if (color) };
writeCString("\033[1m", out); write_names();
if (col.type->shouldAlignRightInPrettyFormats())
{
for (size_t k = 0; k < max_widths[i] - name_widths[i]; ++k)
writeChar(' ', out);
writeString(col.name, out);
}
else
{
writeString(col.name, out);
for (size_t k = 0; k < max_widths[i] - name_widths[i]; ++k)
writeChar(' ', out);
}
if (color)
writeCString("\033[0m", out);
}
writeCString(" ", out);
writeCString(grid_symbols.bold_bar, out);
writeCString("\n", out);
if (format_settings.pretty.output_format_pretty_row_numbers) if (format_settings.pretty.output_format_pretty_row_numbers)
{ {
@ -317,9 +338,15 @@ void PrettyBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port_kind
if (j != 0) if (j != 0)
writeCString(grid_symbols.bar, out); writeCString(grid_symbols.bar, out);
const auto & type = *header.getByPosition(j).type; const auto & type = *header.getByPosition(j).type;
writeValueWithPadding(*columns[j], *serializations[j], i, writeValueWithPadding(
*columns[j],
*serializations[j],
i,
widths[j].empty() ? max_widths[j] : widths[j][i], widths[j].empty() ? max_widths[j] : widths[j][i],
max_widths[j], cut_to_width, type.shouldAlignRightInPrettyFormats(), isNumber(type)); max_widths[j],
cut_to_width,
type.shouldAlignRightInPrettyFormats(),
isNumber(type));
} }
writeCString(grid_symbols.bar, out); writeCString(grid_symbols.bar, out);
@ -332,8 +359,33 @@ void PrettyBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port_kind
/// Write left blank /// Write left blank
writeString(String(row_number_width, ' '), out); writeString(String(row_number_width, ' '), out);
} }
writeString(bottom_separator_s, out);
/// output column names in the footer
if ((num_rows >= format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows) && format_settings.pretty.output_format_pretty_display_footer_column_names)
{
writeString(footer_top_separator_s, out);
if (format_settings.pretty.output_format_pretty_row_numbers)
{
/// Write left blank
writeString(String(row_number_width, ' '), out);
}
/// output header names
write_names();
if (format_settings.pretty.output_format_pretty_row_numbers)
{
/// Write left blank
writeString(String(row_number_width, ' '), out);
}
writeString(footer_bottom_separator_s, out);
}
else
{
writeString(bottom_separator_s, out);
}
total_rows += num_rows; total_rows += num_rows;
} }

View File

@ -57,7 +57,8 @@ PrettyCompactBlockOutputFormat::PrettyCompactBlockOutputFormat(WriteBuffer & out
void PrettyCompactBlockOutputFormat::writeHeader( void PrettyCompactBlockOutputFormat::writeHeader(
const Block & block, const Block & block,
const Widths & max_widths, const Widths & max_widths,
const Widths & name_widths) const Widths & name_widths,
const bool write_footer)
{ {
if (format_settings.pretty.output_format_pretty_row_numbers) if (format_settings.pretty.output_format_pretty_row_numbers)
{ {
@ -70,14 +71,20 @@ void PrettyCompactBlockOutputFormat::writeHeader(
ascii_grid_symbols; ascii_grid_symbols;
/// Names /// Names
writeCString(grid_symbols.left_top_corner, out); if (write_footer)
writeCString(grid_symbols.left_bottom_corner, out);
else
writeCString(grid_symbols.left_top_corner, out);
writeCString(grid_symbols.dash, out); writeCString(grid_symbols.dash, out);
for (size_t i = 0; i < max_widths.size(); ++i) for (size_t i = 0; i < max_widths.size(); ++i)
{ {
if (i != 0) if (i != 0)
{ {
writeCString(grid_symbols.dash, out); writeCString(grid_symbols.dash, out);
writeCString(grid_symbols.top_separator, out); if (write_footer)
writeCString(grid_symbols.bottom_separator, out);
else
writeCString(grid_symbols.top_separator, out);
writeCString(grid_symbols.dash, out); writeCString(grid_symbols.dash, out);
} }
@ -107,7 +114,10 @@ void PrettyCompactBlockOutputFormat::writeHeader(
} }
} }
writeCString(grid_symbols.dash, out); writeCString(grid_symbols.dash, out);
writeCString(grid_symbols.right_top_corner, out); if (write_footer)
writeCString(grid_symbols.right_bottom_corner, out);
else
writeCString(grid_symbols.right_top_corner, out);
writeCString("\n", out); writeCString("\n", out);
} }
@ -195,13 +205,19 @@ void PrettyCompactBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind po
Widths name_widths; Widths name_widths;
calculateWidths(header, chunk, widths, max_widths, name_widths); calculateWidths(header, chunk, widths, max_widths, name_widths);
writeHeader(header, max_widths, name_widths); writeHeader(header, max_widths, name_widths, false);
for (size_t i = 0; i < num_rows && total_rows + i < max_rows; ++i) for (size_t i = 0; i < num_rows && total_rows + i < max_rows; ++i)
writeRow(i, header, chunk, widths, max_widths); writeRow(i, header, chunk, widths, max_widths);
if ((num_rows >= format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows) && format_settings.pretty.output_format_pretty_display_footer_column_names)
writeBottom(max_widths); {
writeHeader(header, max_widths, name_widths, true);
}
else
{
writeBottom(max_widths);
}
total_rows += num_rows; total_rows += num_rows;
} }

View File

@ -17,7 +17,7 @@ public:
String getName() const override { return "PrettyCompactBlockOutputFormat"; } String getName() const override { return "PrettyCompactBlockOutputFormat"; }
private: private:
void writeHeader(const Block & block, const Widths & max_widths, const Widths & name_widths); void writeHeader(const Block & block, const Widths & max_widths, const Widths & name_widths, bool write_footer);
void writeBottom(const Widths & max_widths); void writeBottom(const Widths & max_widths);
void writeRow( void writeRow(
size_t row_num, size_t row_num,

View File

@ -36,39 +36,46 @@ void PrettySpaceBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port
if (format_settings.pretty.output_format_pretty_row_numbers) if (format_settings.pretty.output_format_pretty_row_numbers)
writeString(String(row_number_width, ' '), out); writeString(String(row_number_width, ' '), out);
/// Names /// Names
for (size_t i = 0; i < num_columns; ++i) auto write_names = [&](const bool is_footer) -> void
{ {
if (i != 0) for (size_t i = 0; i < num_columns; ++i)
writeCString(" ", out);
else
writeChar(' ', out);
const ColumnWithTypeAndName & col = header.getByPosition(i);
if (col.type->shouldAlignRightInPrettyFormats())
{ {
for (ssize_t k = 0; k < std::max(0z, static_cast<ssize_t>(max_widths[i] - name_widths[i])); ++k) if (i != 0)
writeCString(" ", out);
else
writeChar(' ', out); writeChar(' ', out);
if (color) const ColumnWithTypeAndName & col = header.getByPosition(i);
writeCString("\033[1m", out);
writeString(col.name, out);
if (color)
writeCString("\033[0m", out);
}
else
{
if (color)
writeCString("\033[1m", out);
writeString(col.name, out);
if (color)
writeCString("\033[0m", out);
for (ssize_t k = 0; k < std::max(0z, static_cast<ssize_t>(max_widths[i] - name_widths[i])); ++k) if (col.type->shouldAlignRightInPrettyFormats())
writeChar(' ', out); {
for (ssize_t k = 0; k < std::max(0z, static_cast<ssize_t>(max_widths[i] - name_widths[i])); ++k)
writeChar(' ', out);
if (color)
writeCString("\033[1m", out);
writeString(col.name, out);
if (color)
writeCString("\033[0m", out);
}
else
{
if (color)
writeCString("\033[1m", out);
writeString(col.name, out);
if (color)
writeCString("\033[0m", out);
for (ssize_t k = 0; k < std::max(0z, static_cast<ssize_t>(max_widths[i] - name_widths[i])); ++k)
writeChar(' ', out);
}
} }
} if (!is_footer)
writeCString("\n\n", out); writeCString("\n\n", out);
else
writeCString("\n", out);
};
write_names(false);
for (size_t row = 0; row < num_rows && total_rows + row < max_rows; ++row) for (size_t row = 0; row < num_rows && total_rows + row < max_rows; ++row)
{ {
@ -95,11 +102,19 @@ void PrettySpaceBlockOutputFormat::writeChunk(const Chunk & chunk, PortKind port
writeValueWithPadding( writeValueWithPadding(
*columns[column], *serializations[column], row, cur_width, max_widths[column], cut_to_width, type.shouldAlignRightInPrettyFormats(), isNumber(type)); *columns[column], *serializations[column], row, cur_width, max_widths[column], cut_to_width, type.shouldAlignRightInPrettyFormats(), isNumber(type));
} }
writeReadableNumberTip(chunk); writeReadableNumberTip(chunk);
writeChar('\n', out); writeChar('\n', out);
} }
/// Write blank line between last row and footer
if ((num_rows >= format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows) && format_settings.pretty.output_format_pretty_display_footer_column_names)
writeCString("\n", out);
/// Write left blank
if ((num_rows >= format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows) && format_settings.pretty.output_format_pretty_row_numbers && format_settings.pretty.output_format_pretty_display_footer_column_names)
writeString(String(row_number_width, ' '), out);
/// Write footer
if ((num_rows >= format_settings.pretty.output_format_pretty_display_footer_column_names_min_rows) && format_settings.pretty.output_format_pretty_display_footer_column_names)
write_names(true);
total_rows += num_rows; total_rows += num_rows;
} }

View File

@ -30,7 +30,6 @@
#include <Common/scope_guard_safe.h> #include <Common/scope_guard_safe.h>
#include <Common/setThreadName.h> #include <Common/setThreadName.h>
#include <Common/typeid_cast.h> #include <Common/typeid_cast.h>
#include <Common/re2.h>
#include <Parsers/ASTSetQuery.h> #include <Parsers/ASTSetQuery.h>
#include <Processors/Formats/IOutputFormat.h> #include <Processors/Formats/IOutputFormat.h>
#include <Formats/FormatFactory.h> #include <Formats/FormatFactory.h>
@ -44,6 +43,7 @@
#include <Poco/Base64Decoder.h> #include <Poco/Base64Decoder.h>
#include <Poco/Base64Encoder.h> #include <Poco/Base64Encoder.h>
#include <Poco/Net/HTTPBasicCredentials.h> #include <Poco/Net/HTTPBasicCredentials.h>
#include <Poco/Net/HTTPMessage.h>
#include <Poco/Net/HTTPStream.h> #include <Poco/Net/HTTPStream.h>
#include <Poco/MemoryStream.h> #include <Poco/MemoryStream.h>
#include <Poco/StreamCopier.h> #include <Poco/StreamCopier.h>
@ -53,7 +53,10 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <optional>
#include <sstream> #include <sstream>
#include <unordered_map>
#include <utility>
#if USE_SSL #if USE_SSL
#include <Poco/Net/X509Certificate.h> #include <Poco/Net/X509Certificate.h>
@ -338,11 +341,11 @@ void HTTPHandler::pushDelayedResults(Output & used_output)
} }
HTTPHandler::HTTPHandler(IServer & server_, const std::string & name, const std::optional<String> & content_type_override_) HTTPHandler::HTTPHandler(IServer & server_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_)
: server(server_) : server(server_)
, log(getLogger(name)) , log(getLogger(name))
, default_settings(server.context()->getSettingsRef()) , default_settings(server.context()->getSettingsRef())
, content_type_override(content_type_override_) , http_response_headers_override(http_response_headers_override_)
{ {
server_display_name = server.config().getString("display_name", getFQDNOrHostName()); server_display_name = server.config().getString("display_name", getFQDNOrHostName());
} }
@ -670,8 +673,7 @@ void HTTPHandler::processQuery(
{ {
auto tmp_data = std::make_shared<TemporaryDataOnDisk>(server.context()->getTempDataOnDisk()); auto tmp_data = std::make_shared<TemporaryDataOnDisk>(server.context()->getTempDataOnDisk());
auto create_tmp_disk_buffer = [tmp_data] (const WriteBufferPtr &) -> WriteBufferPtr auto create_tmp_disk_buffer = [tmp_data] (const WriteBufferPtr &) -> WriteBufferPtr {
{
return tmp_data->createRawStream(); return tmp_data->createRawStream();
}; };
@ -893,13 +895,14 @@ void HTTPHandler::processQuery(
customizeContext(request, context, *in_post_maybe_compressed); customizeContext(request, context, *in_post_maybe_compressed);
in = has_external_data ? std::move(in_param) : std::make_unique<ConcatReadBuffer>(*in_param, *in_post_maybe_compressed); in = has_external_data ? std::move(in_param) : std::make_unique<ConcatReadBuffer>(*in_param, *in_post_maybe_compressed);
applyHTTPResponseHeaders(response, http_response_headers_override);
auto set_query_result = [&response, this] (const QueryResultDetails & details) auto set_query_result = [&response, this] (const QueryResultDetails & details)
{ {
response.add("X-ClickHouse-Query-Id", details.query_id); response.add("X-ClickHouse-Query-Id", details.query_id);
if (content_type_override) if (!(http_response_headers_override && http_response_headers_override->contains(Poco::Net::HTTPMessage::CONTENT_TYPE))
response.setContentType(*content_type_override); && details.content_type)
else if (details.content_type)
response.setContentType(*details.content_type); response.setContentType(*details.content_type);
if (details.format) if (details.format)
@ -1185,8 +1188,9 @@ void HTTPHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse
used_output.finalize(); used_output.finalize();
} }
DynamicQueryHandler::DynamicQueryHandler(IServer & server_, const std::string & param_name_, const std::optional<String>& content_type_override_) DynamicQueryHandler::DynamicQueryHandler(
: HTTPHandler(server_, "DynamicQueryHandler", content_type_override_), param_name(param_name_) IServer & server_, const std::string & param_name_, const HTTPResponseHeaderSetup & http_response_headers_override_)
: HTTPHandler(server_, "DynamicQueryHandler", http_response_headers_override_), param_name(param_name_)
{ {
} }
@ -1247,8 +1251,8 @@ PredefinedQueryHandler::PredefinedQueryHandler(
const std::string & predefined_query_, const std::string & predefined_query_,
const CompiledRegexPtr & url_regex_, const CompiledRegexPtr & url_regex_,
const std::unordered_map<String, CompiledRegexPtr> & header_name_with_regex_, const std::unordered_map<String, CompiledRegexPtr> & header_name_with_regex_,
const std::optional<String> & content_type_override_) const HTTPResponseHeaderSetup & http_response_headers_override_)
: HTTPHandler(server_, "PredefinedQueryHandler", content_type_override_) : HTTPHandler(server_, "PredefinedQueryHandler", http_response_headers_override_)
, receive_params(receive_params_) , receive_params(receive_params_)
, predefined_query(predefined_query_) , predefined_query(predefined_query_)
, url_regex(url_regex_) , url_regex(url_regex_)
@ -1340,14 +1344,10 @@ HTTPRequestHandlerFactoryPtr createDynamicHandlerFactory(IServer & server,
{ {
auto query_param_name = config.getString(config_prefix + ".handler.query_param_name", "query"); auto query_param_name = config.getString(config_prefix + ".handler.query_param_name", "query");
std::optional<String> content_type_override; HTTPResponseHeaderSetup http_response_headers_override = parseHTTPResponseHeaders(config, config_prefix);
if (config.has(config_prefix + ".handler.content_type"))
content_type_override = config.getString(config_prefix + ".handler.content_type");
auto creator = [&server, query_param_name, content_type_override] () -> std::unique_ptr<DynamicQueryHandler> auto creator = [&server, query_param_name, http_response_headers_override]() -> std::unique_ptr<DynamicQueryHandler>
{ { return std::make_unique<DynamicQueryHandler>(server, query_param_name, http_response_headers_override); };
return std::make_unique<DynamicQueryHandler>(server, query_param_name, content_type_override);
};
auto factory = std::make_shared<HandlingRuleHTTPHandlerFactory<DynamicQueryHandler>>(std::move(creator)); auto factory = std::make_shared<HandlingRuleHTTPHandlerFactory<DynamicQueryHandler>>(std::move(creator));
factory->addFiltersFromConfig(config, config_prefix); factory->addFiltersFromConfig(config, config_prefix);
@ -1402,9 +1402,7 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
headers_name_with_regex.emplace(std::make_pair(header_name, regex)); headers_name_with_regex.emplace(std::make_pair(header_name, regex));
} }
std::optional<String> content_type_override; HTTPResponseHeaderSetup http_response_headers_override = parseHTTPResponseHeaders(config, config_prefix);
if (config.has(config_prefix + ".handler.content_type"))
content_type_override = config.getString(config_prefix + ".handler.content_type");
std::shared_ptr<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>> factory; std::shared_ptr<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>> factory;
@ -1424,12 +1422,12 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
predefined_query, predefined_query,
regex, regex,
headers_name_with_regex, headers_name_with_regex,
content_type_override] http_response_headers_override]
-> std::unique_ptr<PredefinedQueryHandler> -> std::unique_ptr<PredefinedQueryHandler>
{ {
return std::make_unique<PredefinedQueryHandler>( return std::make_unique<PredefinedQueryHandler>(
server, analyze_receive_params, predefined_query, regex, server, analyze_receive_params, predefined_query, regex,
headers_name_with_regex, content_type_override); headers_name_with_regex, http_response_headers_override);
}; };
factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator)); factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator));
factory->addFiltersFromConfig(config, config_prefix); factory->addFiltersFromConfig(config, config_prefix);
@ -1442,12 +1440,12 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
analyze_receive_params, analyze_receive_params,
predefined_query, predefined_query,
headers_name_with_regex, headers_name_with_regex,
content_type_override] http_response_headers_override]
-> std::unique_ptr<PredefinedQueryHandler> -> std::unique_ptr<PredefinedQueryHandler>
{ {
return std::make_unique<PredefinedQueryHandler>( return std::make_unique<PredefinedQueryHandler>(
server, analyze_receive_params, predefined_query, CompiledRegexPtr{}, server, analyze_receive_params, predefined_query, CompiledRegexPtr{},
headers_name_with_regex, content_type_override); headers_name_with_regex, http_response_headers_override);
}; };
factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator)); factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator));

View File

@ -1,5 +1,8 @@
#pragma once #pragma once
#include <optional>
#include <string>
#include <unordered_map>
#include <Core/Names.h> #include <Core/Names.h>
#include <Server/HTTP/HTMLForm.h> #include <Server/HTTP/HTMLForm.h>
#include <Server/HTTP/HTTPRequestHandler.h> #include <Server/HTTP/HTTPRequestHandler.h>
@ -10,6 +13,8 @@
#include <Compression/CompressedWriteBuffer.h> #include <Compression/CompressedWriteBuffer.h>
#include <Common/re2.h> #include <Common/re2.h>
#include "HTTPResponseHeaderWriter.h"
namespace CurrentMetrics namespace CurrentMetrics
{ {
extern const Metric HTTPConnection; extern const Metric HTTPConnection;
@ -31,7 +36,7 @@ using CompiledRegexPtr = std::shared_ptr<const re2::RE2>;
class HTTPHandler : public HTTPRequestHandler class HTTPHandler : public HTTPRequestHandler
{ {
public: public:
HTTPHandler(IServer & server_, const std::string & name, const std::optional<String> & content_type_override_); HTTPHandler(IServer & server_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_);
~HTTPHandler() override; ~HTTPHandler() override;
void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override;
@ -113,8 +118,8 @@ private:
/// See settings http_max_fields, http_max_field_name_size, http_max_field_value_size in HTMLForm. /// See settings http_max_fields, http_max_field_name_size, http_max_field_value_size in HTMLForm.
const Settings & default_settings; const Settings & default_settings;
/// Overrides Content-Type provided by the format of the response. /// Overrides for response headers.
std::optional<String> content_type_override; HTTPResponseHeaderSetup http_response_headers_override;
// session is reset at the end of each request/response. // session is reset at the end of each request/response.
std::unique_ptr<Session> session; std::unique_ptr<Session> session;
@ -162,8 +167,12 @@ class DynamicQueryHandler : public HTTPHandler
{ {
private: private:
std::string param_name; std::string param_name;
public: public:
explicit DynamicQueryHandler(IServer & server_, const std::string & param_name_ = "query", const std::optional<String>& content_type_override_ = std::nullopt); explicit DynamicQueryHandler(
IServer & server_,
const std::string & param_name_ = "query",
const HTTPResponseHeaderSetup & http_response_headers_override_ = std::nullopt);
std::string getQuery(HTTPServerRequest & request, HTMLForm & params, ContextMutablePtr context) override; std::string getQuery(HTTPServerRequest & request, HTMLForm & params, ContextMutablePtr context) override;
@ -177,11 +186,15 @@ private:
std::string predefined_query; std::string predefined_query;
CompiledRegexPtr url_regex; CompiledRegexPtr url_regex;
std::unordered_map<String, CompiledRegexPtr> header_name_with_capture_regex; std::unordered_map<String, CompiledRegexPtr> header_name_with_capture_regex;
public: public:
PredefinedQueryHandler( PredefinedQueryHandler(
IServer & server_, const NameSet & receive_params_, const std::string & predefined_query_ IServer & server_,
, const CompiledRegexPtr & url_regex_, const std::unordered_map<String, CompiledRegexPtr> & header_name_with_regex_ const NameSet & receive_params_,
, const std::optional<std::string> & content_type_override_); const std::string & predefined_query_,
const CompiledRegexPtr & url_regex_,
const std::unordered_map<String, CompiledRegexPtr> & header_name_with_regex_,
const HTTPResponseHeaderSetup & http_response_headers_override_ = std::nullopt);
void customizeContext(HTTPServerRequest & request, ContextMutablePtr context, ReadBuffer & body) override; void customizeContext(HTTPServerRequest & request, ContextMutablePtr context, ReadBuffer & body) override;

View File

@ -74,7 +74,8 @@ static auto createPingHandlerFactory(IServer & server)
auto creator = [&server]() -> std::unique_ptr<StaticRequestHandler> auto creator = [&server]() -> std::unique_ptr<StaticRequestHandler>
{ {
constexpr auto ping_response_expression = "Ok.\n"; constexpr auto ping_response_expression = "Ok.\n";
return std::make_unique<StaticRequestHandler>(server, ping_response_expression); return std::make_unique<StaticRequestHandler>(
server, ping_response_expression, parseHTTPResponseHeaders("text/html; charset=UTF-8"));
}; };
return std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(creator)); return std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(creator));
} }
@ -214,7 +215,8 @@ void addCommonDefaultHandlersFactory(HTTPRequestHandlerFactoryMain & factory, IS
auto root_creator = [&server]() -> std::unique_ptr<StaticRequestHandler> auto root_creator = [&server]() -> std::unique_ptr<StaticRequestHandler>
{ {
constexpr auto root_response_expression = "config://http_server_default_response"; constexpr auto root_response_expression = "config://http_server_default_response";
return std::make_unique<StaticRequestHandler>(server, root_response_expression); return std::make_unique<StaticRequestHandler>(
server, root_response_expression, parseHTTPResponseHeaders("text/html; charset=UTF-8"));
}; };
auto root_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(root_creator)); auto root_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(root_creator));
root_handler->attachStrictPath("/"); root_handler->attachStrictPath("/");

View File

@ -0,0 +1,69 @@
#include "HTTPResponseHeaderWriter.h"
#include <unordered_map>
#include <utility>
#include <Poco/Net/HTTPMessage.h>
namespace DB
{
std::unordered_map<String, String>
baseParseHTTPResponseHeaders(const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix)
{
std::unordered_map<String, String> http_response_headers_override;
String http_response_headers_key = config_prefix + ".handler.http_response_headers";
String http_response_headers_key_prefix = http_response_headers_key + ".";
if (config.has(http_response_headers_key))
{
Poco::Util::AbstractConfiguration::Keys keys;
config.keys(http_response_headers_key, keys);
for (const auto & key : keys)
{
http_response_headers_override[key] = config.getString(http_response_headers_key_prefix + key);
}
}
if (config.has(config_prefix + ".handler.content_type"))
http_response_headers_override[Poco::Net::HTTPMessage::CONTENT_TYPE] = config.getString(config_prefix + ".handler.content_type");
return http_response_headers_override;
}
HTTPResponseHeaderSetup parseHTTPResponseHeaders(const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix)
{
std::unordered_map<String, String> http_response_headers_override = baseParseHTTPResponseHeaders(config, config_prefix);
if (http_response_headers_override.empty())
return {};
return std::move(http_response_headers_override);
}
std::unordered_map<String, String> parseHTTPResponseHeaders(
const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix, const std::string & default_content_type)
{
std::unordered_map<String, String> http_response_headers_override = baseParseHTTPResponseHeaders(config, config_prefix);
if (!http_response_headers_override.contains(Poco::Net::HTTPMessage::CONTENT_TYPE))
http_response_headers_override[Poco::Net::HTTPMessage::CONTENT_TYPE] = default_content_type;
return http_response_headers_override;
}
std::unordered_map<String, String> parseHTTPResponseHeaders(const std::string & default_content_type)
{
return {{{Poco::Net::HTTPMessage::CONTENT_TYPE, default_content_type}}};
}
void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const HTTPResponseHeaderSetup & setup)
{
if (setup)
for (const auto & [header_name, header_value] : *setup)
response.set(header_name, header_value);
}
void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const std::unordered_map<String, String> & setup)
{
for (const auto & [header_name, header_value] : setup)
response.set(header_name, header_value);
}
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <optional>
#include <string>
#include <unordered_map>
#include <base/types.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Util/AbstractConfiguration.h>
namespace DB
{
using HTTPResponseHeaderSetup = std::optional<std::unordered_map<String, String>>;
HTTPResponseHeaderSetup parseHTTPResponseHeaders(const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix);
std::unordered_map<String, String> parseHTTPResponseHeaders(
const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix, const std::string & default_content_type);
std::unordered_map<String, String> parseHTTPResponseHeaders(const std::string & default_content_type);
void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const HTTPResponseHeaderSetup & setup);
void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const std::unordered_map<String, String> & setup);
}

View File

@ -2,7 +2,7 @@
#include "IServer.h" #include "IServer.h"
#include "HTTPHandlerFactory.h" #include "HTTPHandlerFactory.h"
#include "HTTPHandlerRequestFilter.h" #include "HTTPResponseHeaderWriter.h"
#include <IO/HTTPCommon.h> #include <IO/HTTPCommon.h>
#include <IO/ReadBufferFromFile.h> #include <IO/ReadBufferFromFile.h>
@ -14,6 +14,7 @@
#include <Common/Exception.h> #include <Common/Exception.h>
#include <unordered_map>
#include <Poco/Net/HTTPServerRequest.h> #include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPServerResponse.h> #include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Net/HTTPRequestHandlerFactory.h> #include <Poco/Net/HTTPRequestHandlerFactory.h>
@ -94,7 +95,7 @@ void StaticRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServer
try try
{ {
response.setContentType(content_type); applyHTTPResponseHeaders(response, http_response_headers_override);
if (request.getVersion() == Poco::Net::HTTPServerRequest::HTTP_1_1) if (request.getVersion() == Poco::Net::HTTPServerRequest::HTTP_1_1)
response.setChunkedTransferEncoding(true); response.setChunkedTransferEncoding(true);
@ -155,8 +156,9 @@ void StaticRequestHandler::writeResponse(WriteBuffer & out)
writeString(response_expression, out); writeString(response_expression, out);
} }
StaticRequestHandler::StaticRequestHandler(IServer & server_, const String & expression, int status_, const String & content_type_) StaticRequestHandler::StaticRequestHandler(
: server(server_), status(status_), content_type(content_type_), response_expression(expression) IServer & server_, const String & expression, const std::unordered_map<String, String> & http_response_headers_override_, int status_)
: server(server_), status(status_), http_response_headers_override(http_response_headers_override_), response_expression(expression)
{ {
} }
@ -166,12 +168,12 @@ HTTPRequestHandlerFactoryPtr createStaticHandlerFactory(IServer & server,
{ {
int status = config.getInt(config_prefix + ".handler.status", 200); int status = config.getInt(config_prefix + ".handler.status", 200);
std::string response_content = config.getRawString(config_prefix + ".handler.response_content", "Ok.\n"); std::string response_content = config.getRawString(config_prefix + ".handler.response_content", "Ok.\n");
std::string response_content_type = config.getString(config_prefix + ".handler.content_type", "text/plain; charset=UTF-8");
auto creator = [&server, response_content, status, response_content_type]() -> std::unique_ptr<StaticRequestHandler> std::unordered_map<String, String> http_response_headers_override
{ = parseHTTPResponseHeaders(config, config_prefix, "text/plain; charset=UTF-8");
return std::make_unique<StaticRequestHandler>(server, response_content, status, response_content_type);
}; auto creator = [&server, http_response_headers_override, response_content, status]() -> std::unique_ptr<StaticRequestHandler>
{ return std::make_unique<StaticRequestHandler>(server, response_content, http_response_headers_override, status); };
auto factory = std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(creator)); auto factory = std::make_shared<HandlingRuleHTTPHandlerFactory<StaticRequestHandler>>(std::move(creator));

View File

@ -1,9 +1,9 @@
#pragma once #pragma once
#include <unordered_map>
#include <Server/HTTP/HTTPRequestHandler.h> #include <Server/HTTP/HTTPRequestHandler.h>
#include <base/types.h> #include <base/types.h>
namespace DB namespace DB
{ {
@ -17,15 +17,16 @@ private:
IServer & server; IServer & server;
int status; int status;
String content_type; /// Overrides for response headers.
std::unordered_map<String, String> http_response_headers_override;
String response_expression; String response_expression;
public: public:
StaticRequestHandler( StaticRequestHandler(
IServer & server, IServer & server,
const String & expression, const String & expression,
int status_ = 200, const std::unordered_map<String, String> & http_response_headers_override_,
const String & content_type_ = "text/html; charset=UTF-8"); int status_ = 200);
void writeResponse(WriteBuffer & out); void writeResponse(WriteBuffer & out);

View File

@ -310,7 +310,7 @@ ReplicatedMergeMutateTaskBase::PrepareResult MergeFromLogEntryTask::prepare()
auto table_id = storage.getStorageID(); auto table_id = storage.getStorageID();
task_context = Context::createCopy(storage.getContext()); task_context = Context::createCopy(storage.getContext());
task_context->makeQueryContext(); task_context->makeQueryContextForMerge(*storage.getSettings());
task_context->setCurrentQueryId(getQueryId()); task_context->setCurrentQueryId(getQueryId());
task_context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MERGE); task_context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MERGE);

View File

@ -165,7 +165,7 @@ void MergePlainMergeTreeTask::finish()
ContextMutablePtr MergePlainMergeTreeTask::createTaskContext() const ContextMutablePtr MergePlainMergeTreeTask::createTaskContext() const
{ {
auto context = Context::createCopy(storage.getContext()); auto context = Context::createCopy(storage.getContext());
context->makeQueryContext(); context->makeQueryContextForMerge(*storage.getSettings());
auto queryId = getQueryId(); auto queryId = getQueryId();
context->setCurrentQueryId(queryId); context->setCurrentQueryId(queryId);
context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MERGE); context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MERGE);

View File

@ -138,7 +138,7 @@ private:
virtual ~IStage() = default; virtual ~IStage() = default;
}; };
/// By default this context is uninitialed, but some variables has to be set after construction, /// By default this context is uninitialized, but some variables has to be set after construction,
/// some variables are used in a process of execution /// some variables are used in a process of execution
/// Proper initialization is responsibility of the author /// Proper initialization is responsibility of the author
struct GlobalRuntimeContext : public IStageRuntimeContext struct GlobalRuntimeContext : public IStageRuntimeContext
@ -199,7 +199,7 @@ private:
using GlobalRuntimeContextPtr = std::shared_ptr<GlobalRuntimeContext>; using GlobalRuntimeContextPtr = std::shared_ptr<GlobalRuntimeContext>;
/// By default this context is uninitialed, but some variables has to be set after construction, /// By default this context is uninitialized, but some variables has to be set after construction,
/// some variables are used in a process of execution /// some variables are used in a process of execution
/// Proper initialization is responsibility of the author /// Proper initialization is responsibility of the author
struct ExecuteAndFinalizeHorizontalPartRuntimeContext : public IStageRuntimeContext struct ExecuteAndFinalizeHorizontalPartRuntimeContext : public IStageRuntimeContext
@ -273,7 +273,7 @@ private:
GlobalRuntimeContextPtr global_ctx; GlobalRuntimeContextPtr global_ctx;
}; };
/// By default this context is uninitialed, but some variables has to be set after construction, /// By default this context is uninitialized, but some variables has to be set after construction,
/// some variables are used in a process of execution /// some variables are used in a process of execution
/// Proper initialization is responsibility of the author /// Proper initialization is responsibility of the author
struct VerticalMergeRuntimeContext : public IStageRuntimeContext struct VerticalMergeRuntimeContext : public IStageRuntimeContext
@ -348,7 +348,7 @@ private:
GlobalRuntimeContextPtr global_ctx; GlobalRuntimeContextPtr global_ctx;
}; };
/// By default this context is uninitialed, but some variables has to be set after construction, /// By default this context is uninitialized, but some variables has to be set after construction,
/// some variables are used in a process of execution /// some variables are used in a process of execution
/// Proper initialization is responsibility of the author /// Proper initialization is responsibility of the author
struct MergeProjectionsRuntimeContext : public IStageRuntimeContext struct MergeProjectionsRuntimeContext : public IStageRuntimeContext

View File

@ -81,6 +81,8 @@ struct Settings;
M(UInt64, min_delay_to_mutate_ms, 10, "Min delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations", 0) \ M(UInt64, min_delay_to_mutate_ms, 10, "Min delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations", 0) \
M(UInt64, max_delay_to_mutate_ms, 1000, "Max delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations", 0) \ M(UInt64, max_delay_to_mutate_ms, 1000, "Max delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations", 0) \
M(Bool, exclude_deleted_rows_for_part_size_in_merge, false, "Use an estimated source part size (excluding lightweight deleted rows) when selecting parts to merge", 0) \ M(Bool, exclude_deleted_rows_for_part_size_in_merge, false, "Use an estimated source part size (excluding lightweight deleted rows) when selecting parts to merge", 0) \
M(String, merge_workload, "", "Name of workload to be used to access resources for merges", 0) \
M(String, mutation_workload, "", "Name of workload to be used to access resources for mutations", 0) \
\ \
/** Inserts settings. */ \ /** Inserts settings. */ \
M(UInt64, parts_to_delay_insert, 1000, "If table contains at least that many active parts in single partition, artificially slow down insert into table. Disabled if set to 0", 0) \ M(UInt64, parts_to_delay_insert, 1000, "If table contains at least that many active parts in single partition, artificially slow down insert into table. Disabled if set to 0", 0) \

View File

@ -204,7 +204,7 @@ ReplicatedMergeMutateTaskBase::PrepareResult MutateFromLogEntryTask::prepare()
} }
task_context = Context::createCopy(storage.getContext()); task_context = Context::createCopy(storage.getContext());
task_context->makeQueryContext(); task_context->makeQueryContextForMutate(*storage.getSettings());
task_context->setCurrentQueryId(getQueryId()); task_context->setCurrentQueryId(getQueryId());
task_context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MUTATION); task_context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MUTATION);

View File

@ -136,7 +136,7 @@ bool MutatePlainMergeTreeTask::executeStep()
ContextMutablePtr MutatePlainMergeTreeTask::createTaskContext() const ContextMutablePtr MutatePlainMergeTreeTask::createTaskContext() const
{ {
auto context = Context::createCopy(storage.getContext()); auto context = Context::createCopy(storage.getContext());
context->makeQueryContext(); context->makeQueryContextForMutate(*storage.getSettings());
auto queryId = getQueryId(); auto queryId = getQueryId();
context->setCurrentQueryId(queryId); context->setCurrentQueryId(queryId);
context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MUTATION); context->setBackgroundOperationTypeForContext(ClientInfo::BackgroundOperationType::MUTATION);

View File

@ -132,11 +132,11 @@ void WriteBufferFromHDFS::sync()
} }
void WriteBufferFromHDFS::finalizeImpl() WriteBufferFromHDFS::~WriteBufferFromHDFS()
{ {
try try
{ {
next(); finalize();
} }
catch (...) catch (...)
{ {
@ -144,11 +144,5 @@ void WriteBufferFromHDFS::finalizeImpl()
} }
} }
WriteBufferFromHDFS::~WriteBufferFromHDFS()
{
finalize();
}
} }
#endif #endif

View File

@ -38,8 +38,6 @@ public:
std::string getFileName() const override { return filename; } std::string getFileName() const override { return filename; }
private: private:
void finalizeImpl() override;
struct WriteBufferFromHDFSImpl; struct WriteBufferFromHDFSImpl;
std::unique_ptr<WriteBufferFromHDFSImpl> impl; std::unique_ptr<WriteBufferFromHDFSImpl> impl;
const std::string filename; const std::string filename;

View File

@ -83,7 +83,6 @@ void StorageObjectStorageSink::finalize()
{ {
writer->finalize(); writer->finalize();
writer->flush(); writer->flush();
write_buf->finalize();
} }
catch (...) catch (...)
{ {
@ -91,6 +90,8 @@ void StorageObjectStorageSink::finalize()
release(); release();
throw; throw;
} }
write_buf->finalize();
} }
void StorageObjectStorageSink::release() void StorageObjectStorageSink::release()

View File

@ -1823,7 +1823,6 @@ private:
{ {
writer->finalize(); writer->finalize();
writer->flush(); writer->flush();
write_buf->finalize();
} }
catch (...) catch (...)
{ {
@ -1831,12 +1830,14 @@ private:
release(); release();
throw; throw;
} }
write_buf->finalize();
} }
void release() void release()
{ {
writer.reset(); writer.reset();
write_buf->finalize(); write_buf.reset();
} }
StorageMetadataPtr metadata_snapshot; StorageMetadataPtr metadata_snapshot;

View File

@ -609,7 +609,6 @@ void StorageURLSink::finalize()
{ {
writer->finalize(); writer->finalize();
writer->flush(); writer->flush();
write_buf->finalize();
} }
catch (...) catch (...)
{ {
@ -617,12 +616,14 @@ void StorageURLSink::finalize()
release(); release();
throw; throw;
} }
write_buf->finalize();
} }
void StorageURLSink::release() void StorageURLSink::release()
{ {
writer.reset(); writer.reset();
write_buf->finalize(); write_buf.reset();
} }
class PartitionedStorageURLSink : public PartitionedSink class PartitionedStorageURLSink : public PartitionedSink

View File

@ -81,7 +81,10 @@ void StorageSystemServerSettings::fillData(MutableColumns & res_columns, Context
{"uncompressed_cache_size", {std::to_string(context->getUncompressedCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}}, {"uncompressed_cache_size", {std::to_string(context->getUncompressedCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}},
{"index_mark_cache_size", {std::to_string(context->getIndexMarkCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}}, {"index_mark_cache_size", {std::to_string(context->getIndexMarkCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}},
{"index_uncompressed_cache_size", {std::to_string(context->getIndexUncompressedCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}}, {"index_uncompressed_cache_size", {std::to_string(context->getIndexUncompressedCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}},
{"mmap_cache_size", {std::to_string(context->getMMappedFileCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}} {"mmap_cache_size", {std::to_string(context->getMMappedFileCache()->maxSizeInBytes()), ChangeableWithoutRestart::Yes}},
{"merge_workload", {context->getMergeWorkload(), ChangeableWithoutRestart::Yes}},
{"mutation_workload", {context->getMutationWorkload(), ChangeableWithoutRestart::Yes}}
}; };
if (context->areBackgroundExecutorsInitialized()) if (context->areBackgroundExecutorsInitialized())

View File

@ -9,3 +9,5 @@
01287_max_execution_speed 01287_max_execution_speed
# Check after ConstantNode refactoring # Check after ConstantNode refactoring
02154_parser_backtracking 02154_parser_backtracking
02944_variant_as_common_type
02942_variant_cast

View File

@ -15,7 +15,7 @@ from github.Commit import Commit
from build_download_helper import download_build_with_progress from build_download_helper import download_build_with_progress
from commit_status_helper import post_commit_status from commit_status_helper import post_commit_status
from compress_files import SUFFIX, compress_fast, decompress_fast from compress_files import SUFFIX, compress_fast, decompress_fast
from env_helper import CI, RUNNER_TEMP, S3_BUILDS_BUCKET from env_helper import IS_CI, RUNNER_TEMP, S3_BUILDS_BUCKET
from git_helper import SHA_REGEXP from git_helper import SHA_REGEXP
from report import FOOTER_HTML_TEMPLATE, HEAD_HTML_TEMPLATE, SUCCESS from report import FOOTER_HTML_TEMPLATE, HEAD_HTML_TEMPLATE, SUCCESS
from s3_helper import S3Helper from s3_helper import S3Helper
@ -131,7 +131,7 @@ class ArtifactsHelper:
post_commit_status(commit, SUCCESS, url, "Artifacts for workflow", "Artifacts") post_commit_status(commit, SUCCESS, url, "Artifacts for workflow", "Artifacts")
def _regenerate_index(self) -> None: def _regenerate_index(self) -> None:
if CI: if IS_CI:
files = self._get_s3_objects() files = self._get_s3_objects()
else: else:
files = self._get_local_s3_objects() files = self._get_local_s3_objects()

View File

@ -6,7 +6,7 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from build_download_helper import get_build_name_for_check, read_build_urls from build_download_helper import read_build_urls
from clickhouse_helper import CiLogsCredentials from clickhouse_helper import CiLogsCredentials
from docker_images_helper import DockerImage, get_docker_image, pull_image from docker_images_helper import DockerImage, get_docker_image, pull_image
from env_helper import REPORT_PATH, TEMP_PATH from env_helper import REPORT_PATH, TEMP_PATH
@ -14,6 +14,7 @@ from pr_info import PRInfo
from report import FAIL, FAILURE, OK, SUCCESS, JobReport, TestResult from report import FAIL, FAILURE, OK, SUCCESS, JobReport, TestResult
from stopwatch import Stopwatch from stopwatch import Stopwatch
from tee_popen import TeePopen from tee_popen import TeePopen
from ci_config import CI
IMAGE_NAME = "clickhouse/fuzzer" IMAGE_NAME = "clickhouse/fuzzer"
@ -64,7 +65,7 @@ def main():
docker_image = pull_image(get_docker_image(IMAGE_NAME)) docker_image = pull_image(get_docker_image(IMAGE_NAME))
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
urls = read_build_urls(build_name, reports_path) urls = read_build_urls(build_name, reports_path)
if not urls: if not urls:
raise ValueError("No build URLs found") raise ValueError("No build URLs found")

View File

@ -7,7 +7,7 @@ import sys
from pathlib import Path from pathlib import Path
from typing import List, Sequence, Tuple from typing import List, Sequence, Tuple
from ci_config import JobNames from ci_config import CI
from ci_utils import normalize_string from ci_utils import normalize_string
from env_helper import TEMP_PATH from env_helper import TEMP_PATH
from functional_test_check import NO_CHANGES_MSG from functional_test_check import NO_CHANGES_MSG
@ -92,16 +92,19 @@ def main():
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# args = parse_args() # args = parse_args()
stopwatch = Stopwatch() stopwatch = Stopwatch()
jobs_to_validate = [JobNames.STATELESS_TEST_RELEASE, JobNames.INTEGRATION_TEST] jobs_to_validate = [
CI.JobNames.STATELESS_TEST_RELEASE,
CI.JobNames.INTEGRATION_TEST,
]
functional_job_report_file = Path(TEMP_PATH) / "functional_test_job_report.json" functional_job_report_file = Path(TEMP_PATH) / "functional_test_job_report.json"
integration_job_report_file = Path(TEMP_PATH) / "integration_test_job_report.json" integration_job_report_file = Path(TEMP_PATH) / "integration_test_job_report.json"
jobs_report_files = { jobs_report_files = {
JobNames.STATELESS_TEST_RELEASE: functional_job_report_file, CI.JobNames.STATELESS_TEST_RELEASE: functional_job_report_file,
JobNames.INTEGRATION_TEST: integration_job_report_file, CI.JobNames.INTEGRATION_TEST: integration_job_report_file,
} }
jobs_scripts = { jobs_scripts = {
JobNames.STATELESS_TEST_RELEASE: "functional_test_check.py", CI.JobNames.STATELESS_TEST_RELEASE: "functional_test_check.py",
JobNames.INTEGRATION_TEST: "integration_test_check.py", CI.JobNames.INTEGRATION_TEST: "integration_test_check.py",
} }
for test_job in jobs_to_validate: for test_job in jobs_to_validate:

View File

@ -9,7 +9,7 @@ from pathlib import Path
from typing import Tuple from typing import Tuple
import docker_images_helper import docker_images_helper
from ci_config import CI_CONFIG, BuildConfig from ci_config import CI
from env_helper import REPO_COPY, S3_BUILDS_BUCKET, TEMP_PATH from env_helper import REPO_COPY, S3_BUILDS_BUCKET, TEMP_PATH
from git_helper import Git from git_helper import Git
from lambda_shared_package.lambda_shared.pr import Labels from lambda_shared_package.lambda_shared.pr import Labels
@ -27,7 +27,7 @@ IMAGE_NAME = "clickhouse/binary-builder"
BUILD_LOG_NAME = "build_log.log" BUILD_LOG_NAME = "build_log.log"
def _can_export_binaries(build_config: BuildConfig) -> bool: def _can_export_binaries(build_config: CI.BuildConfig) -> bool:
if build_config.package_type != "deb": if build_config.package_type != "deb":
return False return False
if build_config.sanitizer != "": if build_config.sanitizer != "":
@ -38,7 +38,7 @@ def _can_export_binaries(build_config: BuildConfig) -> bool:
def get_packager_cmd( def get_packager_cmd(
build_config: BuildConfig, build_config: CI.BuildConfig,
packager_path: Path, packager_path: Path,
output_path: Path, output_path: Path,
build_version: str, build_version: str,
@ -147,7 +147,8 @@ def main():
stopwatch = Stopwatch() stopwatch = Stopwatch()
build_name = args.build_name build_name = args.build_name
build_config = CI_CONFIG.build_config[build_name] build_config = CI.JOB_CONFIGS[build_name].build_config
assert build_config
temp_path = Path(TEMP_PATH) temp_path = Path(TEMP_PATH)
temp_path.mkdir(parents=True, exist_ok=True) temp_path.mkdir(parents=True, exist_ok=True)

View File

@ -10,7 +10,7 @@ from typing import Any, Callable, List, Optional, Union
import requests import requests
from ci_config import CI_CONFIG from ci_config import CI
try: try:
# A work around for scripts using this downloading module without required deps # A work around for scripts using this downloading module without required deps
@ -122,10 +122,6 @@ def get_gh_api(
raise APIException(f"Unable to request data from GH API: {url}") from exc raise APIException(f"Unable to request data from GH API: {url}") from exc
def get_build_name_for_check(check_name: str) -> str:
return CI_CONFIG.test_configs[check_name].required_build
def read_build_urls(build_name: str, reports_path: Union[Path, str]) -> List[str]: def read_build_urls(build_name: str, reports_path: Union[Path, str]) -> List[str]:
for root, _, files in os.walk(reports_path): for root, _, files in os.walk(reports_path):
for file in files: for file in files:
@ -210,7 +206,7 @@ def download_builds_filter(
result_path: Path, result_path: Path,
filter_fn: Callable[[str], bool] = lambda _: True, filter_fn: Callable[[str], bool] = lambda _: True,
) -> None: ) -> None:
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
urls = read_build_urls(build_name, reports_path) urls = read_build_urls(build_name, reports_path)
logger.info("The build report for %s contains the next URLs: %s", build_name, urls) logger.info("The build report for %s contains the next URLs: %s", build_name, urls)
@ -247,7 +243,7 @@ def download_clickhouse_binary(
def get_clickhouse_binary_url( def get_clickhouse_binary_url(
check_name: str, reports_path: Union[Path, str] check_name: str, reports_path: Union[Path, str]
) -> Optional[str]: ) -> Optional[str]:
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
urls = read_build_urls(build_name, reports_path) urls = read_build_urls(build_name, reports_path)
logger.info("The build report for %s contains the next URLs: %s", build_name, urls) logger.info("The build report for %s contains the next URLs: %s", build_name, urls)
for url in urls: for url in urls:

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import json import json
import logging import logging
import os import os
@ -6,7 +7,6 @@ import sys
from pathlib import Path from pathlib import Path
from typing import List from typing import List
from ci_config import CI_CONFIG, Build
from env_helper import ( from env_helper import (
GITHUB_JOB_URL, GITHUB_JOB_URL,
GITHUB_REPOSITORY, GITHUB_REPOSITORY,
@ -14,7 +14,7 @@ from env_helper import (
REPORT_PATH, REPORT_PATH,
TEMP_PATH, TEMP_PATH,
CI_CONFIG_PATH, CI_CONFIG_PATH,
CI, IS_CI,
) )
from pr_info import PRInfo from pr_info import PRInfo
from report import ( from report import (
@ -25,8 +25,10 @@ from report import (
JobReport, JobReport,
create_build_html_report, create_build_html_report,
get_worst_status, get_worst_status,
FAILURE,
) )
from stopwatch import Stopwatch from stopwatch import Stopwatch
from ci_config import CI
# Old way to read the neads_data # Old way to read the neads_data
NEEDS_DATA_PATH = os.getenv("NEEDS_DATA_PATH", "") NEEDS_DATA_PATH = os.getenv("NEEDS_DATA_PATH", "")
@ -46,16 +48,13 @@ def main():
"\n ".join(p.as_posix() for p in reports_path.rglob("*.json")), "\n ".join(p.as_posix() for p in reports_path.rglob("*.json")),
) )
build_check_name = sys.argv[1] build_check_name = CI.JobNames.BUILD_CHECK
pr_info = PRInfo() pr_info = PRInfo()
builds_for_check = CI_CONFIG.get_builds_for_report( args = parse_args()
build_check_name,
release=pr_info.is_release, if (CI_CONFIG_PATH or IS_CI) and not args.reports:
backport=pr_info.head_ref.startswith("backport/"),
)
if CI:
# In CI only specific builds might be manually selected, or some wf does not build all builds. # In CI only specific builds might be manually selected, or some wf does not build all builds.
# Filtering @builds_for_check to verify only builds that are present in the current CI workflow # Filtering @builds_for_check to verify only builds that are present in the current CI workflow
with open(CI_CONFIG_PATH, encoding="utf-8") as jfd: with open(CI_CONFIG_PATH, encoding="utf-8") as jfd:
@ -64,8 +63,12 @@ def main():
ci_config["jobs_data"]["jobs_to_skip"] ci_config["jobs_data"]["jobs_to_skip"]
+ ci_config["jobs_data"]["jobs_to_do"] + ci_config["jobs_data"]["jobs_to_do"]
) )
builds_for_check = [job for job in builds_for_check if job in all_ci_jobs] builds_for_check = [job for job in CI.BuildNames if job in all_ci_jobs]
print(f"NOTE: following build reports will be accounted: [{builds_for_check}]") print(f"NOTE: following build reports will be checked: [{builds_for_check}]")
else:
builds_for_check = parse_args().reports
for job in builds_for_check:
assert job in CI.BuildNames, "Builds must be known build job names"
required_builds = len(builds_for_check) required_builds = len(builds_for_check)
missing_builds = 0 missing_builds = 0
@ -77,8 +80,8 @@ def main():
build_name, pr_info.number, pr_info.head_ref build_name, pr_info.number, pr_info.head_ref
) )
if not build_result: if not build_result:
if build_name == Build.FUZZERS: if build_name == CI.BuildNames.FUZZERS:
logging.info("Build [%s] is missing - skip", Build.FUZZERS) logging.info("Build [%s] is missing - skip", CI.BuildNames.FUZZERS)
continue continue
logging.warning("Build results for %s is missing", build_name) logging.warning("Build results for %s is missing", build_name)
build_result = BuildResult.missing_result("missing") build_result = BuildResult.missing_result("missing")
@ -132,17 +135,16 @@ def main():
# Check if there are no builds at all, do not override bad status # Check if there are no builds at all, do not override bad status
if summary_status == SUCCESS: if summary_status == SUCCESS:
if missing_builds: if missing_builds:
summary_status = PENDING summary_status = FAILURE
elif ok_groups == 0: elif ok_groups == 0:
summary_status = ERROR summary_status = ERROR
addition = "" description = ""
if missing_builds:
addition = (
f" ({required_builds - missing_builds} of {required_builds} builds are OK)"
)
description = f"{ok_groups}/{total_groups} artifact groups are OK{addition}" if missing_builds:
description = f"{missing_builds} of {required_builds} builds are missing."
description += f" {ok_groups}/{total_groups} artifact groups are OK"
JobReport( JobReport(
description=description, description=description,
@ -158,5 +160,16 @@ def main():
sys.exit(1) sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser("Generates overall build report")
parser.add_argument(
"--reports",
nargs="+",
help="List of build reports to check",
)
return parser.parse_args()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -13,14 +13,7 @@ from typing import Any, Dict, List, Optional
import docker_images_helper import docker_images_helper
import upload_result_helper import upload_result_helper
from build_check import get_release_or_pr from build_check import get_release_or_pr
from ci_config import ( from ci_config import CI
CI_CONFIG,
Build,
CILabels,
CIStages,
JobNames,
StatusNames,
)
from ci_metadata import CiMetadata from ci_metadata import CiMetadata
from ci_utils import GHActions, normalize_string from ci_utils import GHActions, normalize_string
from clickhouse_helper import ( from clickhouse_helper import (
@ -38,10 +31,11 @@ from commit_status_helper import (
get_commit, get_commit,
post_commit_status, post_commit_status,
set_status_comment, set_status_comment,
get_commit_filtered_statuses,
) )
from digest_helper import DockerDigester from digest_helper import DockerDigester
from env_helper import ( from env_helper import (
CI, IS_CI,
GITHUB_JOB_API_URL, GITHUB_JOB_API_URL,
GITHUB_REPOSITORY, GITHUB_REPOSITORY,
GITHUB_RUN_ID, GITHUB_RUN_ID,
@ -295,7 +289,7 @@ def _mark_success_action(
batch: int, batch: int,
) -> None: ) -> None:
ci_cache = CiCache(s3, indata["jobs_data"]["digests"]) ci_cache = CiCache(s3, indata["jobs_data"]["digests"])
job_config = CI_CONFIG.get_job_config(job) job_config = CI.get_job_config(job)
num_batches = job_config.num_batches num_batches = job_config.num_batches
# if batch is not provided - set to 0 # if batch is not provided - set to 0
batch = 0 if batch == -1 else batch batch = 0 if batch == -1 else batch
@ -305,7 +299,7 @@ def _mark_success_action(
# FIXME: find generic design for propagating and handling job status (e.g. stop using statuses in GH api) # FIXME: find generic design for propagating and handling job status (e.g. stop using statuses in GH api)
# now job ca be build job w/o status data, any other job that exit with 0 with or w/o status data # now job ca be build job w/o status data, any other job that exit with 0 with or w/o status data
if CI_CONFIG.is_build_job(job): if CI.is_build_job(job):
# there is no CommitStatus for build jobs # there is no CommitStatus for build jobs
# create dummy status relying on JobReport # create dummy status relying on JobReport
# FIXME: consider creating commit status for build jobs too, to treat everything the same way # FIXME: consider creating commit status for build jobs too, to treat everything the same way
@ -425,6 +419,7 @@ def _configure_jobs(
pr_info: PRInfo, pr_info: PRInfo,
ci_settings: CiSettings, ci_settings: CiSettings,
skip_jobs: bool, skip_jobs: bool,
dry_run: bool = False,
) -> CiCache: ) -> CiCache:
""" """
returns CICache instance with configured job's data returns CICache instance with configured job's data
@ -436,10 +431,11 @@ def _configure_jobs(
# get all jobs # get all jobs
if not skip_jobs: if not skip_jobs:
job_configs = CI_CONFIG.get_workflow_jobs_with_configs( job_configs = CI.get_workflow_jobs_with_configs(
is_mq=pr_info.is_merge_queue, is_mq=pr_info.is_merge_queue,
is_docs_only=pr_info.has_changes_in_documentation_only(), is_docs_only=pr_info.has_changes_in_documentation_only(),
is_master=pr_info.is_master, is_master=pr_info.is_master,
is_pr=pr_info.is_pr,
) )
else: else:
job_configs = {} job_configs = {}
@ -457,7 +453,8 @@ def _configure_jobs(
ci_cache = CiCache.calc_digests_and_create( ci_cache = CiCache.calc_digests_and_create(
s3, s3,
job_configs, job_configs,
cache_enabled=not ci_settings.no_ci_cache and not skip_jobs and CI, cache_enabled=not ci_settings.no_ci_cache and not skip_jobs and IS_CI,
dry_run=dry_run,
) )
ci_cache.update() ci_cache.update()
ci_cache.apply(job_configs, is_release=pr_info.is_release) ci_cache.apply(job_configs, is_release=pr_info.is_release)
@ -475,14 +472,14 @@ def _generate_ci_stage_config(jobs_data: Dict[str, Any]) -> Dict[str, Dict[str,
result = {} # type: Dict[str, Any] result = {} # type: Dict[str, Any]
stages_to_do = [] stages_to_do = []
for job in jobs_data: for job in jobs_data:
stage_type = CI_CONFIG.get_job_ci_stage(job) stage_type = CI.get_job_ci_stage(job)
if stage_type == CIStages.NA: if stage_type == CI.WorkflowStages.NA:
continue continue
if stage_type not in result: if stage_type not in result:
result[stage_type] = [] result[stage_type] = []
stages_to_do.append(stage_type) stages_to_do.append(stage_type)
result[stage_type].append( result[stage_type].append(
{"job_name": job, "runner_type": CI_CONFIG.get_runner_type(job)} {"job_name": job, "runner_type": CI.JOB_CONFIGS[job].runner_type}
) )
result["stages_to_do"] = stages_to_do result["stages_to_do"] = stages_to_do
return result return result
@ -529,10 +526,10 @@ def _update_gh_statuses_action(indata: Dict, s3: S3Helper) -> None:
if job not in jobs_to_skip and job not in jobs_to_do: if job not in jobs_to_skip and job not in jobs_to_do:
# no need to create status for job that are not supposed to be executed # no need to create status for job that are not supposed to be executed
continue continue
if CI_CONFIG.is_build_job(job): if CI.is_build_job(job):
# no GH status for build jobs # no GH status for build jobs
continue continue
job_config = CI_CONFIG.get_job_config(job) job_config = CI.get_job_config(job)
if not job_config: if not job_config:
# there might be a new job that does not exist on this branch - skip it # there might be a new job that does not exist on this branch - skip it
continue continue
@ -558,7 +555,7 @@ def _fetch_commit_tokens(message: str, pr_info: PRInfo) -> List[str]:
res = [ res = [
match match
for match in matches for match in matches
if match in CILabels or match.startswith("job_") or match.startswith("batch_") if match in CI.Tags or match.startswith("job_") or match.startswith("batch_")
] ]
print(f"CI modifiers from commit message: [{res}]") print(f"CI modifiers from commit message: [{res}]")
res_2 = [] res_2 = []
@ -567,7 +564,7 @@ def _fetch_commit_tokens(message: str, pr_info: PRInfo) -> List[str]:
res_2 = [ res_2 = [
match match
for match in matches for match in matches
if match in CILabels if match in CI.Tags
or match.startswith("job_") or match.startswith("job_")
or match.startswith("batch_") or match.startswith("batch_")
] ]
@ -643,7 +640,7 @@ def _upload_build_artifacts(
print(f"Report file has been uploaded to [{report_url}]") print(f"Report file has been uploaded to [{report_url}]")
# Upload master head's binaries # Upload master head's binaries
static_bin_name = CI_CONFIG.build_config[build_name].static_binary_name static_bin_name = CI.get_build_config(build_name).static_binary_name
if pr_info.is_master and static_bin_name: if pr_info.is_master and static_bin_name:
# Full binary with debug info: # Full binary with debug info:
s3_path_full = "/".join((pr_info.base_ref, static_bin_name, "clickhouse-full")) s3_path_full = "/".join((pr_info.base_ref, static_bin_name, "clickhouse-full"))
@ -838,15 +835,15 @@ def _add_build_to_version_history(
def _run_test(job_name: str, run_command: str) -> int: def _run_test(job_name: str, run_command: str) -> int:
assert ( assert (
run_command or CI_CONFIG.get_job_config(job_name).run_command run_command or CI.get_job_config(job_name).run_command
), "Run command must be provided as input argument or be configured in job config" ), "Run command must be provided as input argument or be configured in job config"
env = os.environ.copy() env = os.environ.copy()
timeout = CI_CONFIG.get_job_config(job_name).timeout or None timeout = CI.get_job_config(job_name).timeout or None
if not run_command: if not run_command:
run_command = "/".join( run_command = "/".join(
(os.path.dirname(__file__), CI_CONFIG.get_job_config(job_name).run_command) (os.path.dirname(__file__), CI.get_job_config(job_name).run_command)
) )
if ".py" in run_command and not run_command.startswith("python"): if ".py" in run_command and not run_command.startswith("python"):
run_command = "python3 " + run_command run_command = "python3 " + run_command
@ -913,13 +910,23 @@ def _cancel_pr_wf(s3: S3Helper, pr_number: int, cancel_sync: bool = False) -> No
def _set_pending_statuses(pr_info: PRInfo) -> None: def _set_pending_statuses(pr_info: PRInfo) -> None:
commit = get_commit(GitHub(get_best_robot_token(), per_page=100), pr_info.sha) commit = get_commit(GitHub(get_best_robot_token(), per_page=100), pr_info.sha)
try: try:
print("Set SYNC status to pending") found = False
commit.create_status( statuses = get_commit_filtered_statuses(commit)
state=PENDING, for commit_status in statuses:
target_url="", if commit_status.context == CI.StatusNames.SYNC:
description="", print(
context=StatusNames.SYNC, f"Sync status found [{commit_status.state}], [{commit_status.description}] - won't be overwritten"
) )
found = True
break
if not found:
print("Set Sync status to pending")
commit.create_status(
state=PENDING,
target_url="",
description=CI.SyncState.PENDING,
context=CI.StatusNames.SYNC,
)
except Exception as ex: except Exception as ex:
print(f"ERROR: failed to set GH commit status, ex: {ex}") print(f"ERROR: failed to set GH commit status, ex: {ex}")
@ -952,7 +959,7 @@ def main() -> int:
### CONFIGURE action: start ### CONFIGURE action: start
if args.configure: if args.configure:
if CI and pr_info.is_pr: if IS_CI and pr_info.is_pr:
# store meta on s3 (now we need it only for PRs) # store meta on s3 (now we need it only for PRs)
meta = CiMetadata(s3, pr_info.number, pr_info.head_ref) meta = CiMetadata(s3, pr_info.number, pr_info.head_ref)
meta.run_id = int(GITHUB_RUN_ID) meta.run_id = int(GITHUB_RUN_ID)
@ -962,7 +969,7 @@ def main() -> int:
args.commit_message or None, update_from_api=True args.commit_message or None, update_from_api=True
) )
if ci_settings.no_merge_commit and CI: if ci_settings.no_merge_commit and IS_CI:
git_runner.run(f"{GIT_PREFIX} checkout {pr_info.sha}") git_runner.run(f"{GIT_PREFIX} checkout {pr_info.sha}")
git_ref = git_runner.run(f"{GIT_PREFIX} rev-parse HEAD") git_ref = git_runner.run(f"{GIT_PREFIX} rev-parse HEAD")
@ -985,18 +992,19 @@ def main() -> int:
) )
ci_cache.print_status() ci_cache.print_status()
if CI and not pr_info.is_merge_queue: if IS_CI and not pr_info.is_merge_queue:
# wait for pending jobs to be finished, await_jobs is a long blocking call # wait for pending jobs to be finished, await_jobs is a long blocking call
ci_cache.await_pending_jobs(pr_info.is_release) ci_cache.await_pending_jobs(pr_info.is_release)
if pr_info.is_release: if pr_info.is_release:
print("Release/master: CI Cache add pending records for all todo jobs")
ci_cache.push_pending_all(pr_info.is_release) ci_cache.push_pending_all(pr_info.is_release)
# conclude results # conclude results
result["git_ref"] = git_ref result["git_ref"] = git_ref
result["version"] = version result["version"] = version
result["build"] = ci_cache.job_digests[Build.PACKAGE_RELEASE] result["build"] = ci_cache.job_digests[CI.BuildNames.PACKAGE_RELEASE]
result["docs"] = ci_cache.job_digests[JobNames.DOCS_CHECK] result["docs"] = ci_cache.job_digests[CI.JobNames.DOCS_CHECK]
result["ci_settings"] = ci_settings.as_dict() result["ci_settings"] = ci_settings.as_dict()
if not args.skip_jobs: if not args.skip_jobs:
result["stages_data"] = _generate_ci_stage_config(ci_cache.jobs_to_do) result["stages_data"] = _generate_ci_stage_config(ci_cache.jobs_to_do)
@ -1027,7 +1035,7 @@ def main() -> int:
f"Check if rerun for name: [{check_name}], extended name [{check_name_with_group}]" f"Check if rerun for name: [{check_name}], extended name [{check_name_with_group}]"
) )
previous_status = None previous_status = None
if CI_CONFIG.is_build_job(check_name): if CI.is_build_job(check_name):
# this is a build job - check if a build report is present # this is a build job - check if a build report is present
build_result = ( build_result = (
BuildResult.load_any(check_name, pr_info.number, pr_info.head_ref) BuildResult.load_any(check_name, pr_info.number, pr_info.head_ref)
@ -1055,10 +1063,8 @@ def main() -> int:
# rerun helper check # rerun helper check
# FIXME: remove rerun_helper check and rely on ci cache only # FIXME: remove rerun_helper check and rely on ci cache only
if check_name not in ( if check_name not in (
# we might want to rerun reports' jobs - disable rerun check for them CI.JobNames.BUILD_CHECK,
JobNames.BUILD_CHECK, ): # we might want to rerun build report job
JobNames.BUILD_CHECK_SPECIAL,
):
rerun_helper = RerunHelper(commit, check_name_with_group) rerun_helper = RerunHelper(commit, check_name_with_group)
if rerun_helper.is_already_finished_by_status(): if rerun_helper.is_already_finished_by_status():
status = rerun_helper.get_finished_status() status = rerun_helper.get_finished_status()
@ -1071,7 +1077,7 @@ def main() -> int:
# ci cache check # ci cache check
if not previous_status and not ci_settings.no_ci_cache: if not previous_status and not ci_settings.no_ci_cache:
ci_cache = CiCache(s3, indata["jobs_data"]["digests"]).update() ci_cache = CiCache(s3, indata["jobs_data"]["digests"]).update()
job_config = CI_CONFIG.get_job_config(check_name) job_config = CI.get_job_config(check_name)
if ci_cache.is_successful( if ci_cache.is_successful(
check_name, check_name,
args.batch, args.batch,
@ -1111,7 +1117,7 @@ def main() -> int:
ch_helper = ClickHouseHelper() ch_helper = ClickHouseHelper()
check_url = "" check_url = ""
if CI_CONFIG.is_build_job(args.job_name): if CI.is_build_job(args.job_name):
assert ( assert (
indata indata
), f"--infile with config must be provided for POST action of a build type job [{args.job_name}]" ), f"--infile with config must be provided for POST action of a build type job [{args.job_name}]"
@ -1119,8 +1125,7 @@ def main() -> int:
# upload binaries only for normal builds in PRs # upload binaries only for normal builds in PRs
upload_binary = ( upload_binary = (
not pr_info.is_pr not pr_info.is_pr
or args.job_name or CI.get_job_ci_stage(args.job_name) == CI.WorkflowStages.BUILDS_1
not in CI_CONFIG.get_builds_for_report(JobNames.BUILD_CHECK_SPECIAL)
or CiSettings.create_from_run_config(indata).upload_all or CiSettings.create_from_run_config(indata).upload_all
) )

View File

@ -5,7 +5,8 @@ from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Any, Union, Sequence, List, Set from typing import Dict, Optional, Any, Union, Sequence, List, Set
from ci_config import JobNames, Build, CI_CONFIG, JobConfig from ci_config import CI
from ci_utils import is_hex, GHActions from ci_utils import is_hex, GHActions
from commit_status_helper import CommitStatusData from commit_status_helper import CommitStatusData
from env_helper import ( from env_helper import (
@ -41,7 +42,7 @@ class CiCache:
release - for jobs being executed on the release branch including master branch (not a PR branch) release - for jobs being executed on the release branch including master branch (not a PR branch)
""" """
_REQUIRED_DIGESTS = [JobNames.DOCS_CHECK, Build.PACKAGE_RELEASE] _REQUIRED_DIGESTS = [CI.JobNames.DOCS_CHECK, CI.BuildNames.PACKAGE_RELEASE]
_S3_CACHE_PREFIX = "CI_cache_v1" _S3_CACHE_PREFIX = "CI_cache_v1"
_CACHE_BUILD_REPORT_PREFIX = "build_report" _CACHE_BUILD_REPORT_PREFIX = "build_report"
_RECORD_FILE_EXTENSION = ".ci" _RECORD_FILE_EXTENSION = ".ci"
@ -80,7 +81,7 @@ class CiCache:
@classmethod @classmethod
def is_docs_job(cls, job_name: str) -> bool: def is_docs_job(cls, job_name: str) -> bool:
return job_name == JobNames.DOCS_CHECK return job_name == CI.JobNames.DOCS_CHECK
@classmethod @classmethod
def is_srcs_job(cls, job_name: str) -> bool: def is_srcs_job(cls, job_name: str) -> bool:
@ -105,8 +106,8 @@ class CiCache:
): ):
self.enabled = cache_enabled self.enabled = cache_enabled
self.jobs_to_skip = [] # type: List[str] self.jobs_to_skip = [] # type: List[str]
self.jobs_to_wait = {} # type: Dict[str, JobConfig] self.jobs_to_wait = {} # type: Dict[str, CI.JobConfig]
self.jobs_to_do = {} # type: Dict[str, JobConfig] self.jobs_to_do = {} # type: Dict[str, CI.JobConfig]
self.s3 = s3 self.s3 = s3
self.job_digests = job_digests self.job_digests = job_digests
self.cache_s3_paths = { self.cache_s3_paths = {
@ -127,9 +128,13 @@ class CiCache:
@classmethod @classmethod
def calc_digests_and_create( def calc_digests_and_create(
cls, s3: S3Helper, job_configs: Dict[str, JobConfig], cache_enabled: bool = True cls,
s3: S3Helper,
job_configs: Dict[str, CI.JobConfig],
cache_enabled: bool = True,
dry_run: bool = False,
) -> "CiCache": ) -> "CiCache":
job_digester = JobDigester() job_digester = JobDigester(dry_run=dry_run)
digests = {} digests = {}
print("::group::Job Digests") print("::group::Job Digests")
@ -140,9 +145,7 @@ class CiCache:
for job in cls._REQUIRED_DIGESTS: for job in cls._REQUIRED_DIGESTS:
if job not in job_configs: if job not in job_configs:
digest = job_digester.get_job_digest( digest = job_digester.get_job_digest(CI.get_job_config(job).digest)
CI_CONFIG.get_job_config(job).digest
)
digests[job] = digest digests[job] = digest
print( print(
f" job [{job.rjust(50)}] required for CI Cache has digest [{digest}]" f" job [{job.rjust(50)}] required for CI Cache has digest [{digest}]"
@ -154,10 +157,10 @@ class CiCache:
self, job_digests: Dict[str, str], job_type: JobType self, job_digests: Dict[str, str], job_type: JobType
) -> str: ) -> str:
if job_type == self.JobType.DOCS: if job_type == self.JobType.DOCS:
res = job_digests[JobNames.DOCS_CHECK] res = job_digests[CI.JobNames.DOCS_CHECK]
elif job_type == self.JobType.SRCS: elif job_type == self.JobType.SRCS:
if Build.PACKAGE_RELEASE in job_digests: if CI.BuildNames.PACKAGE_RELEASE in job_digests:
res = job_digests[Build.PACKAGE_RELEASE] res = job_digests[CI.BuildNames.PACKAGE_RELEASE]
else: else:
assert False, "BUG, no build job in digest' list" assert False, "BUG, no build job in digest' list"
else: else:
@ -648,7 +651,7 @@ class CiCache:
report_path = Path(REPORT_PATH) report_path = Path(REPORT_PATH)
report_path.mkdir(exist_ok=True, parents=True) report_path.mkdir(exist_ok=True, parents=True)
path = ( path = (
self._get_record_s3_path(Build.PACKAGE_RELEASE) self._get_record_s3_path(CI.BuildNames.PACKAGE_RELEASE)
+ self._CACHE_BUILD_REPORT_PREFIX + self._CACHE_BUILD_REPORT_PREFIX
) )
if file_prefix: if file_prefix:
@ -664,13 +667,14 @@ class CiCache:
def upload_build_report(self, build_result: BuildResult) -> str: def upload_build_report(self, build_result: BuildResult) -> str:
result_json_path = build_result.write_json(Path(TEMP_PATH)) result_json_path = build_result.write_json(Path(TEMP_PATH))
s3_path = ( s3_path = (
self._get_record_s3_path(Build.PACKAGE_RELEASE) + result_json_path.name self._get_record_s3_path(CI.BuildNames.PACKAGE_RELEASE)
+ result_json_path.name
) )
return self.s3.upload_file( return self.s3.upload_file(
bucket=S3_BUILDS_BUCKET, file_path=result_json_path, s3_path=s3_path bucket=S3_BUILDS_BUCKET, file_path=result_json_path, s3_path=s3_path
) )
def await_pending_jobs(self, is_release: bool) -> None: def await_pending_jobs(self, is_release: bool, dry_run: bool = False) -> None:
""" """
await pending jobs to be finished await pending jobs to be finished
@jobs_with_params - jobs to await. {JOB_NAME: {"batches": [BATCHES...], "num_batches": NUM_BATCHES}} @jobs_with_params - jobs to await. {JOB_NAME: {"batches": [BATCHES...], "num_batches": NUM_BATCHES}}
@ -687,15 +691,9 @@ class CiCache:
MAX_JOB_NUM_TO_WAIT = 3 MAX_JOB_NUM_TO_WAIT = 3
round_cnt = 0 round_cnt = 0
# FIXME: temporary experiment: lets enable await for PR' workflows awaiting on build' jobs only # FIXME: temporary experiment: lets enable await for PR' workflows but for a shorter time
if not is_release: if not is_release:
MAX_ROUNDS_TO_WAIT = 1 MAX_ROUNDS_TO_WAIT = 3
remove_from_wait = []
for job in self.jobs_to_wait:
if job not in Build:
remove_from_wait.append(job)
for job in remove_from_wait:
del self.jobs_to_wait[job]
while ( while (
len(self.jobs_to_wait) > MAX_JOB_NUM_TO_WAIT len(self.jobs_to_wait) > MAX_JOB_NUM_TO_WAIT
@ -713,11 +711,12 @@ class CiCache:
start_at = int(time.time()) start_at = int(time.time())
while expired_sec < TIMEOUT and self.jobs_to_wait: while expired_sec < TIMEOUT and self.jobs_to_wait:
await_finished: Set[str] = set() await_finished: Set[str] = set()
time.sleep(poll_interval_sec) if not dry_run:
time.sleep(poll_interval_sec)
self.update() self.update()
for job_name, job_config in self.jobs_to_wait.items(): for job_name, job_config in self.jobs_to_wait.items():
num_batches = job_config.num_batches num_batches = job_config.num_batches
job_config = CI_CONFIG.get_job_config(job_name) job_config = CI.get_job_config(job_name)
assert job_config.pending_batches assert job_config.pending_batches
assert job_config.batches assert job_config.batches
pending_batches = list(job_config.pending_batches) pending_batches = list(job_config.pending_batches)
@ -741,12 +740,11 @@ class CiCache:
f"Job [{job_name}_[{batch}/{num_batches}]] is not pending anymore" f"Job [{job_name}_[{batch}/{num_batches}]] is not pending anymore"
) )
job_config.batches.remove(batch) job_config.batches.remove(batch)
job_config.pending_batches.remove(batch)
else: else:
print( print(
f"NOTE: Job [{job_name}:{batch}] finished failed - do not add to ready" f"NOTE: Job [{job_name}:{batch}] finished failed - do not add to ready"
) )
job_config.pending_batches.remove(batch) job_config.pending_batches.remove(batch)
if not job_config.pending_batches: if not job_config.pending_batches:
await_finished.add(job_name) await_finished.add(job_name)
@ -754,18 +752,25 @@ class CiCache:
for job in await_finished: for job in await_finished:
self.jobs_to_skip.append(job) self.jobs_to_skip.append(job)
del self.jobs_to_wait[job] del self.jobs_to_wait[job]
del self.jobs_to_do[job]
expired_sec = int(time.time()) - start_at if not dry_run:
print( expired_sec = int(time.time()) - start_at
f"...awaiting continues... seconds left [{TIMEOUT - expired_sec}]" print(
) f"...awaiting continues... seconds left [{TIMEOUT - expired_sec}]"
)
else:
# make up for 2 iterations in dry_run
expired_sec += int(TIMEOUT / 2) + 1
GHActions.print_in_group( GHActions.print_in_group(
"Remaining jobs:", "Remaining jobs:",
[list(self.jobs_to_wait)], [list(self.jobs_to_wait)],
) )
def apply(self, job_configs: Dict[str, JobConfig], is_release: bool) -> "CiCache": def apply(
self, job_configs: Dict[str, CI.JobConfig], is_release: bool
) -> "CiCache":
if not self.enabled: if not self.enabled:
self.jobs_to_do = job_configs self.jobs_to_do = job_configs
return self return self

File diff suppressed because it is too large Load Diff

781
tests/ci/ci_definitions.py Normal file
View File

@ -0,0 +1,781 @@
import copy
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, List, Union, Iterable, Optional, Literal, Any
from ci_utils import WithIter
from integration_test_images import IMAGES
class WorkflowStages(metaclass=WithIter):
"""
Stages of GitHUb actions workflow
"""
# for jobs that do not belong to any stage, e.g. Build Report Check
NA = "UNKNOWN"
# normal builds (builds that required for further testing)
BUILDS_1 = "Builds_1"
# special builds
BUILDS_2 = "Builds_2"
# all tests required for merge
TESTS_1 = "Tests_1"
# not used atm
TESTS_2 = "Tests_2"
# all tests not required for merge
TESTS_3 = "Tests_3"
class Runners(metaclass=WithIter):
"""
GitHub runner's labels
"""
BUILDER = "builder"
STYLE_CHECKER = "style-checker"
STYLE_CHECKER_ARM = "style-checker-aarch64"
FUNC_TESTER = "func-tester"
FUNC_TESTER_ARM = "func-tester-aarch64"
STRESS_TESTER = "stress-tester"
FUZZER_UNIT_TESTER = "fuzzer-unit-tester"
class Tags(metaclass=WithIter):
"""
CI Customization tags (set via PR body or some of them in GH labels, e.g. libFuzzer)
"""
DO_NOT_TEST_LABEL = "do_not_test"
NO_MERGE_COMMIT = "no_merge_commit"
NO_CI_CACHE = "no_ci_cache"
# to upload all binaries from build jobs
UPLOAD_ALL_ARTIFACTS = "upload_all"
CI_SET_SYNC = "ci_set_sync"
CI_SET_ARM = "ci_set_arm"
CI_SET_REQUIRED = "ci_set_required"
CI_SET_BUILDS = "ci_set_builds"
CI_SET_NON_REQUIRED = "ci_set_non_required"
CI_SET_OLD_ANALYZER = "ci_set_old_analyzer"
libFuzzer = "libFuzzer"
class BuildNames(metaclass=WithIter):
"""
Build' job names
"""
PACKAGE_RELEASE = "package_release"
PACKAGE_AARCH64 = "package_aarch64"
PACKAGE_ASAN = "package_asan"
PACKAGE_UBSAN = "package_ubsan"
PACKAGE_TSAN = "package_tsan"
PACKAGE_MSAN = "package_msan"
PACKAGE_DEBUG = "package_debug"
PACKAGE_RELEASE_COVERAGE = "package_release_coverage"
BINARY_RELEASE = "binary_release"
BINARY_TIDY = "binary_tidy"
BINARY_DARWIN = "binary_darwin"
BINARY_AARCH64 = "binary_aarch64"
BINARY_AARCH64_V80COMPAT = "binary_aarch64_v80compat"
BINARY_FREEBSD = "binary_freebsd"
BINARY_DARWIN_AARCH64 = "binary_darwin_aarch64"
BINARY_PPC64LE = "binary_ppc64le"
BINARY_AMD64_COMPAT = "binary_amd64_compat"
BINARY_AMD64_MUSL = "binary_amd64_musl"
BINARY_RISCV64 = "binary_riscv64"
BINARY_S390X = "binary_s390x"
BINARY_LOONGARCH64 = "binary_loongarch64"
FUZZERS = "fuzzers"
class JobNames(metaclass=WithIter):
"""
All CI non-build jobs (Build jobs are concatenated to this list via python hack)
"""
STYLE_CHECK = "Style check"
FAST_TEST = "Fast test"
DOCKER_SERVER = "Docker server image"
DOCKER_KEEPER = "Docker keeper image"
INSTALL_TEST_AMD = "Install packages (release)"
INSTALL_TEST_ARM = "Install packages (aarch64)"
STATELESS_TEST_DEBUG = "Stateless tests (debug)"
STATELESS_TEST_RELEASE = "Stateless tests (release)"
STATELESS_TEST_RELEASE_COVERAGE = "Stateless tests (coverage)"
STATELESS_TEST_AARCH64 = "Stateless tests (aarch64)"
STATELESS_TEST_ASAN = "Stateless tests (asan)"
STATELESS_TEST_TSAN = "Stateless tests (tsan)"
STATELESS_TEST_MSAN = "Stateless tests (msan)"
STATELESS_TEST_UBSAN = "Stateless tests (ubsan)"
STATELESS_TEST_OLD_ANALYZER_S3_REPLICATED_RELEASE = (
"Stateless tests (release, old analyzer, s3, DatabaseReplicated)"
)
STATELESS_TEST_S3_DEBUG = "Stateless tests (debug, s3 storage)"
STATELESS_TEST_S3_TSAN = "Stateless tests (tsan, s3 storage)"
STATELESS_TEST_AZURE_ASAN = "Stateless tests (azure, asan)"
STATELESS_TEST_FLAKY_ASAN = "Stateless tests flaky check (asan)"
STATEFUL_TEST_DEBUG = "Stateful tests (debug)"
STATEFUL_TEST_RELEASE = "Stateful tests (release)"
STATEFUL_TEST_RELEASE_COVERAGE = "Stateful tests (coverage)"
STATEFUL_TEST_AARCH64 = "Stateful tests (aarch64)"
STATEFUL_TEST_ASAN = "Stateful tests (asan)"
STATEFUL_TEST_TSAN = "Stateful tests (tsan)"
STATEFUL_TEST_MSAN = "Stateful tests (msan)"
STATEFUL_TEST_UBSAN = "Stateful tests (ubsan)"
STATEFUL_TEST_PARALLEL_REPL_RELEASE = "Stateful tests (release, ParallelReplicas)"
STATEFUL_TEST_PARALLEL_REPL_DEBUG = "Stateful tests (debug, ParallelReplicas)"
STATEFUL_TEST_PARALLEL_REPL_ASAN = "Stateful tests (asan, ParallelReplicas)"
STATEFUL_TEST_PARALLEL_REPL_MSAN = "Stateful tests (msan, ParallelReplicas)"
STATEFUL_TEST_PARALLEL_REPL_UBSAN = "Stateful tests (ubsan, ParallelReplicas)"
STATEFUL_TEST_PARALLEL_REPL_TSAN = "Stateful tests (tsan, ParallelReplicas)"
STRESS_TEST_ASAN = "Stress test (asan)"
STRESS_TEST_TSAN = "Stress test (tsan)"
STRESS_TEST_UBSAN = "Stress test (ubsan)"
STRESS_TEST_MSAN = "Stress test (msan)"
STRESS_TEST_DEBUG = "Stress test (debug)"
STRESS_TEST_AZURE_TSAN = "Stress test (azure, tsan)"
STRESS_TEST_AZURE_MSAN = "Stress test (azure, msan)"
INTEGRATION_TEST = "Integration tests (release)"
INTEGRATION_TEST_ASAN = "Integration tests (asan)"
INTEGRATION_TEST_ASAN_OLD_ANALYZER = "Integration tests (asan, old analyzer)"
INTEGRATION_TEST_TSAN = "Integration tests (tsan)"
INTEGRATION_TEST_ARM = "Integration tests (aarch64)"
INTEGRATION_TEST_FLAKY = "Integration tests flaky check (asan)"
UPGRADE_TEST_DEBUG = "Upgrade check (debug)"
UPGRADE_TEST_ASAN = "Upgrade check (asan)"
UPGRADE_TEST_TSAN = "Upgrade check (tsan)"
UPGRADE_TEST_MSAN = "Upgrade check (msan)"
UNIT_TEST = "Unit tests (release)"
UNIT_TEST_ASAN = "Unit tests (asan)"
UNIT_TEST_MSAN = "Unit tests (msan)"
UNIT_TEST_TSAN = "Unit tests (tsan)"
UNIT_TEST_UBSAN = "Unit tests (ubsan)"
AST_FUZZER_TEST_DEBUG = "AST fuzzer (debug)"
AST_FUZZER_TEST_ASAN = "AST fuzzer (asan)"
AST_FUZZER_TEST_MSAN = "AST fuzzer (msan)"
AST_FUZZER_TEST_TSAN = "AST fuzzer (tsan)"
AST_FUZZER_TEST_UBSAN = "AST fuzzer (ubsan)"
JEPSEN_KEEPER = "ClickHouse Keeper Jepsen"
JEPSEN_SERVER = "ClickHouse Server Jepsen"
PERFORMANCE_TEST_AMD64 = "Performance Comparison (release)"
PERFORMANCE_TEST_ARM64 = "Performance Comparison (aarch64)"
SQL_LOGIC_TEST = "Sqllogic test (release)"
SQLANCER = "SQLancer (release)"
SQLANCER_DEBUG = "SQLancer (debug)"
SQLTEST = "SQLTest"
COMPATIBILITY_TEST = "Compatibility check (release)"
COMPATIBILITY_TEST_ARM = "Compatibility check (aarch64)"
CLICKBENCH_TEST = "ClickBench (release)"
CLICKBENCH_TEST_ARM = "ClickBench (aarch64)"
LIBFUZZER_TEST = "libFuzzer tests"
BUILD_CHECK = "ClickHouse build check"
# BUILD_CHECK_SPECIAL = "ClickHouse special build check"
DOCS_CHECK = "Docs check"
BUGFIX_VALIDATE = "Bugfix validation"
# hack to concatenate Build and non-build jobs under JobNames class
for attr_name in dir(BuildNames):
if not attr_name.startswith("__") and not callable(getattr(BuildNames, attr_name)):
setattr(JobNames, attr_name, getattr(BuildNames, attr_name))
class StatusNames(metaclass=WithIter):
"""
Class with statuses that aren't related to particular jobs
"""
# overall CI report
CI = "CI running"
# mergeable status
MERGEABLE = "Mergeable Check"
# status of a sync pr
SYNC = "A Sync"
# PR formatting check status
PR_CHECK = "PR Check"
class SyncState(metaclass=WithIter):
PENDING = "awaiting merge"
MERGE_FAILED = "merge failed"
TESTING = "awaiting test results"
TESTS_FAILED = "tests failed"
COMPLETED = "completed"
@dataclass
class DigestConfig:
# all files, dirs to include into digest, glob supported
include_paths: List[Union[str, Path]] = field(default_factory=list)
# file suffixes to exclude from digest
exclude_files: List[str] = field(default_factory=list)
# directories to exclude from digest
exclude_dirs: List[Union[str, Path]] = field(default_factory=list)
# docker names to include into digest
docker: List[str] = field(default_factory=list)
# git submodules digest
git_submodules: bool = False
@dataclass
class LabelConfig:
"""
configures different CI scenarios per CI Tag/GH label
"""
run_jobs: Iterable[str] = frozenset()
@dataclass
class BuildConfig:
name: str
compiler: str
package_type: Literal["deb", "binary", "fuzzers"]
additional_pkgs: bool = False
debug_build: bool = False
coverage: bool = False
sanitizer: str = ""
tidy: bool = False
# sparse_checkout is needed only to test the option itself.
# No particular sense to use it in every build, since it slows down the job.
sparse_checkout: bool = False
comment: str = ""
static_binary_name: str = ""
def export_env(self, export: bool = False) -> str:
def process(field_name: str, field: Union[bool, str]) -> str:
if isinstance(field, bool):
field = str(field).lower()
elif not isinstance(field, str):
field = ""
if export:
return f"export BUILD_{field_name.upper()}={repr(field)}"
return f"BUILD_{field_name.upper()}={field}"
return "\n".join(process(k, v) for k, v in self.__dict__.items())
@dataclass
class JobConfig:
"""
contains config parameters for job execution in CI workflow
"""
# GH Runner type (tag from @Runners)
runner_type: str
# used for config validation in ci unittests
job_name_keyword: str = ""
# builds required for the job (applicable for test jobs)
required_builds: Optional[List[str]] = None
# build config for the build job (applicable for builds)
build_config: Optional[BuildConfig] = None
# configures digest calculation for the job
digest: DigestConfig = field(default_factory=DigestConfig)
# will be triggered for the job if omitted in CI workflow yml
run_command: str = ""
# job timeout, seconds
timeout: Optional[int] = None
# sets number of batches for a multi-batch job
num_batches: int = 1
# label that enables job in CI, if set digest isn't used
run_by_label: str = ""
# to run always regardless of the job digest or/and label
run_always: bool = False
# if the job needs to be run on the release branch, including master (building packages, docker server).
# NOTE: Subsequent runs on the same branch with the similar digest are still considered skip-able.
required_on_release_branch: bool = False
# job is for pr workflow only
pr_only: bool = False
# job is for release/master branches only
release_only: bool = False
# to randomly pick and run one job among jobs in the same @random_bucket (PR branches only).
random_bucket: str = ""
# Do not set it. A list of batches to run. It will be set in runtime in accordance with ci cache and ci settings
batches: Optional[List[int]] = None
# Do not set it. A list of batches to await. It will be set in runtime in accordance with ci cache and ci settings
pending_batches: Optional[List[int]] = None
def with_properties(self, **kwargs: Any) -> "JobConfig":
res = copy.deepcopy(self)
for k, v in kwargs.items():
assert hasattr(self, k), f"Setting invalid attribute [{k}]"
setattr(res, k, v)
return res
def get_required_build(self) -> str:
assert self.required_builds
return self.required_builds[0]
class CommonJobConfigs:
"""
Common job configs
"""
BUILD_REPORT = JobConfig(
job_name_keyword="build_check",
run_command="build_report_check.py",
digest=DigestConfig(
include_paths=[
"./tests/ci/build_report_check.py",
"./tests/ci/upload_result_helper.py",
],
),
runner_type=Runners.STYLE_CHECKER_ARM,
)
COMPATIBILITY_TEST = JobConfig(
job_name_keyword="compatibility",
digest=DigestConfig(
include_paths=["./tests/ci/compatibility_check.py"],
docker=["clickhouse/test-old-ubuntu", "clickhouse/test-old-centos"],
),
run_command="compatibility_check.py",
runner_type=Runners.STYLE_CHECKER,
)
INSTALL_TEST = JobConfig(
job_name_keyword="install",
digest=DigestConfig(
include_paths=["./tests/ci/install_check.py"],
docker=["clickhouse/install-deb-test", "clickhouse/install-rpm-test"],
),
run_command='install_check.py "$CHECK_NAME"',
runner_type=Runners.STYLE_CHECKER,
timeout=900,
)
STATELESS_TEST = JobConfig(
job_name_keyword="stateless",
digest=DigestConfig(
include_paths=[
"./tests/ci/functional_test_check.py",
"./tests/queries/0_stateless/",
"./tests/clickhouse-test",
"./tests/config",
"./tests/*.txt",
],
exclude_files=[".md"],
docker=["clickhouse/stateless-test"],
),
run_command='functional_test_check.py "$CHECK_NAME"',
runner_type=Runners.FUNC_TESTER,
timeout=10800,
)
STATEFUL_TEST = JobConfig(
job_name_keyword="stateful",
digest=DigestConfig(
include_paths=[
"./tests/ci/functional_test_check.py",
"./tests/queries/1_stateful/",
"./tests/clickhouse-test",
"./tests/config",
"./tests/*.txt",
],
exclude_files=[".md"],
docker=["clickhouse/stateful-test"],
),
run_command='functional_test_check.py "$CHECK_NAME"',
runner_type=Runners.FUNC_TESTER,
timeout=3600,
)
STRESS_TEST = JobConfig(
job_name_keyword="stress",
digest=DigestConfig(
include_paths=[
"./tests/queries/0_stateless/",
"./tests/queries/1_stateful/",
"./tests/clickhouse-test",
"./tests/config",
"./tests/*.txt",
],
exclude_files=[".md"],
docker=["clickhouse/stress-test"],
),
run_command="stress_check.py",
runner_type=Runners.STRESS_TESTER,
timeout=9000,
)
UPGRADE_TEST = JobConfig(
job_name_keyword="upgrade",
digest=DigestConfig(
include_paths=["./tests/ci/upgrade_check.py"],
exclude_files=[".md"],
docker=["clickhouse/upgrade-check"],
),
run_command="upgrade_check.py",
runner_type=Runners.STRESS_TESTER,
)
INTEGRATION_TEST = JobConfig(
job_name_keyword="integration",
digest=DigestConfig(
include_paths=[
"./tests/ci/integration_test_check.py",
"./tests/ci/integration_tests_runner.py",
"./tests/integration/",
],
exclude_files=[".md"],
docker=IMAGES.copy(),
),
run_command='integration_test_check.py "$CHECK_NAME"',
runner_type=Runners.STRESS_TESTER,
)
ASTFUZZER_TEST = JobConfig(
job_name_keyword="ast",
digest=DigestConfig(),
run_command="ast_fuzzer_check.py",
run_always=True,
runner_type=Runners.FUZZER_UNIT_TESTER,
)
UNIT_TEST = JobConfig(
job_name_keyword="unit",
digest=DigestConfig(
include_paths=["./tests/ci/unit_tests_check.py"],
exclude_files=[".md"],
docker=["clickhouse/unit-test"],
),
run_command="unit_tests_check.py",
runner_type=Runners.FUZZER_UNIT_TESTER,
)
PERF_TESTS = JobConfig(
job_name_keyword="performance",
digest=DigestConfig(
include_paths=[
"./tests/ci/performance_comparison_check.py",
"./tests/performance/",
],
exclude_files=[".md"],
docker=["clickhouse/performance-comparison"],
),
run_command="performance_comparison_check.py",
runner_type=Runners.STRESS_TESTER,
)
SQLLANCER_TEST = JobConfig(
job_name_keyword="lancer",
digest=DigestConfig(),
run_command="sqlancer_check.py",
release_only=True,
run_always=True,
runner_type=Runners.FUZZER_UNIT_TESTER,
)
SQLLOGIC_TEST = JobConfig(
job_name_keyword="logic",
digest=DigestConfig(
include_paths=["./tests/ci/sqllogic_test.py"],
exclude_files=[".md"],
docker=["clickhouse/sqllogic-test"],
),
run_command="sqllogic_test.py",
timeout=10800,
release_only=True,
runner_type=Runners.STYLE_CHECKER,
)
SQL_TEST = JobConfig(
job_name_keyword="sqltest",
digest=DigestConfig(
include_paths=["./tests/ci/sqltest.py"],
exclude_files=[".md"],
docker=["clickhouse/sqltest"],
),
run_command="sqltest.py",
timeout=10800,
release_only=True,
runner_type=Runners.FUZZER_UNIT_TESTER,
)
BUGFIX_TEST = JobConfig(
job_name_keyword="bugfix",
digest=DigestConfig(),
run_command="bugfix_validate_check.py",
timeout=900,
runner_type=Runners.FUNC_TESTER,
)
DOCKER_SERVER = JobConfig(
job_name_keyword="docker",
required_on_release_branch=True,
run_command='docker_server.py --check-name "$CHECK_NAME" --release-type head --allow-build-reuse',
digest=DigestConfig(
include_paths=[
"tests/ci/docker_server.py",
"./docker/server",
]
),
runner_type=Runners.STYLE_CHECKER,
)
CLICKBENCH_TEST = JobConfig(
job_name_keyword="clickbench",
digest=DigestConfig(
include_paths=[
"tests/ci/clickbench.py",
],
docker=["clickhouse/clickbench"],
),
run_command='clickbench.py "$CHECK_NAME"',
timeout=900,
runner_type=Runners.FUNC_TESTER,
)
BUILD = JobConfig(
required_on_release_branch=True,
digest=DigestConfig(
include_paths=[
"./src",
"./contrib/*-cmake",
"./contrib/consistent-hashing",
"./contrib/murmurhash",
"./contrib/libfarmhash",
"./contrib/pdqsort",
"./contrib/cityhash102",
"./contrib/sparse-checkout",
"./contrib/libmetrohash",
"./contrib/update-submodules.sh",
"./contrib/CMakeLists.txt",
"./CMakeLists.txt",
"./PreLoad.cmake",
"./cmake",
"./base",
"./programs",
"./packages",
"./docker/packager/packager",
"./rust",
"./tests/ci/version_helper.py",
# FIXME: This is a WA to rebuild the CH and recreate the Performance.tar.zst artifact
# when there are changes in performance test scripts.
# Due to the current design of the perf test we need to rebuild CH when the performance test changes,
# otherwise the changes will not be visible in the PerformanceTest job in CI
"./tests/performance",
],
exclude_files=[".md"],
docker=["clickhouse/binary-builder"],
git_submodules=True,
),
run_command="build_check.py $BUILD_NAME",
runner_type=Runners.BUILDER,
)
REQUIRED_CHECKS = [
StatusNames.PR_CHECK,
StatusNames.SYNC,
JobNames.BUILD_CHECK,
JobNames.DOCS_CHECK,
JobNames.FAST_TEST,
JobNames.STATEFUL_TEST_RELEASE,
JobNames.STATELESS_TEST_RELEASE,
JobNames.STATELESS_TEST_ASAN,
JobNames.STATELESS_TEST_FLAKY_ASAN,
JobNames.STATEFUL_TEST_ASAN,
JobNames.STYLE_CHECK,
JobNames.UNIT_TEST_ASAN,
JobNames.UNIT_TEST_MSAN,
JobNames.UNIT_TEST,
JobNames.UNIT_TEST_TSAN,
JobNames.UNIT_TEST_UBSAN,
JobNames.INTEGRATION_TEST_ASAN_OLD_ANALYZER,
JobNames.STATELESS_TEST_OLD_ANALYZER_S3_REPLICATED_RELEASE,
]
# Jobs that run in Merge Queue if it's enabled
MQ_JOBS = [
JobNames.STYLE_CHECK,
JobNames.FAST_TEST,
BuildNames.BINARY_RELEASE,
JobNames.UNIT_TEST,
]
@dataclass
class CheckDescription:
name: str
description: str # the check descriptions, will be put into the status table
match_func: Callable[[str], bool] # the function to check vs the commit status
def __hash__(self) -> int:
return hash(self.name + self.description)
CHECK_DESCRIPTIONS = [
CheckDescription(
StatusNames.PR_CHECK,
"Checks correctness of the PR's body",
lambda x: x == "PR Check",
),
CheckDescription(
StatusNames.SYNC,
"If it fails, ask a maintainer for help",
lambda x: x == StatusNames.SYNC,
),
CheckDescription(
"AST fuzzer",
"Runs randomly generated queries to catch program errors. "
"The build type is optionally given in parenthesis. "
"If it fails, ask a maintainer for help",
lambda x: x.startswith("AST fuzzer"),
),
CheckDescription(
JobNames.BUGFIX_VALIDATE,
"Checks that either a new test (functional or integration) or there "
"some changed tests that fail with the binary built on master branch",
lambda x: x == JobNames.BUGFIX_VALIDATE,
),
CheckDescription(
StatusNames.CI,
"A meta-check that indicates the running CI. Normally, it's in <b>success</b> or "
"<b>pending</b> state. The failed status indicates some problems with the PR",
lambda x: x == "CI running",
),
CheckDescription(
"ClickHouse build check",
"Builds ClickHouse in various configurations for use in further steps. "
"You have to fix the builds that fail. Build logs often has enough "
"information to fix the error, but you might have to reproduce the failure "
"locally. The <b>cmake</b> options can be found in the build log, grepping for "
'<b>cmake</b>. Use these options and follow the <a href="'
'https://clickhouse.com/docs/en/development/build">general build process</a>',
lambda x: x.startswith("ClickHouse") and x.endswith("build check"),
),
CheckDescription(
"Compatibility check",
"Checks that <b>clickhouse</b> binary runs on distributions with old libc "
"versions. If it fails, ask a maintainer for help",
lambda x: x.startswith("Compatibility check"),
),
CheckDescription(
JobNames.DOCKER_SERVER,
"The check to build and optionally push the mentioned image to docker hub",
lambda x: x.startswith("Docker server"),
),
CheckDescription(
JobNames.DOCKER_KEEPER,
"The check to build and optionally push the mentioned image to docker hub",
lambda x: x.startswith("Docker keeper"),
),
CheckDescription(
JobNames.DOCS_CHECK,
"Builds and tests the documentation",
lambda x: x == JobNames.DOCS_CHECK,
),
CheckDescription(
JobNames.FAST_TEST,
"Normally this is the first check that is ran for a PR. It builds ClickHouse "
'and runs most of <a href="https://clickhouse.com/docs/en/development/tests'
'#functional-tests">stateless functional tests</a>, '
"omitting some. If it fails, further checks are not started until it is fixed. "
"Look at the report to see which tests fail, then reproduce the failure "
'locally as described <a href="https://clickhouse.com/docs/en/development/'
'tests#functional-test-locally">here</a>',
lambda x: x == JobNames.FAST_TEST,
),
CheckDescription(
"Flaky tests",
"Checks if new added or modified tests are flaky by running them repeatedly, "
"in parallel, with more randomization. Functional tests are run 100 times "
"with address sanitizer, and additional randomization of thread scheduling. "
"Integration tests are run up to 10 times. If at least once a new test has "
"failed, or was too long, this check will be red. We don't allow flaky tests, "
'read <a href="https://clickhouse.com/blog/decorating-a-christmas-tree-with-'
'the-help-of-flaky-tests/">the doc</a>',
lambda x: "tests flaky check" in x,
),
CheckDescription(
"Install packages",
"Checks that the built packages are installable in a clear environment",
lambda x: x.startswith("Install packages ("),
),
CheckDescription(
"Integration tests",
"The integration tests report. In parenthesis the package type is given, "
"and in square brackets are the optional part/total tests",
lambda x: x.startswith("Integration tests ("),
),
CheckDescription(
StatusNames.MERGEABLE,
"Checks if all other necessary checks are successful",
lambda x: x == StatusNames.MERGEABLE,
),
CheckDescription(
"Performance Comparison",
"Measure changes in query performance. The performance test report is "
'described in detail <a href="https://github.com/ClickHouse/ClickHouse/tree'
'/master/docker/test/performance-comparison#how-to-read-the-report">here</a>. '
"In square brackets are the optional part/total tests",
lambda x: x.startswith("Performance Comparison"),
),
CheckDescription(
"Push to Dockerhub",
"The check for building and pushing the CI related docker images to docker hub",
lambda x: x.startswith("Push") and "to Dockerhub" in x,
),
CheckDescription(
"Sqllogic",
"Run clickhouse on the "
'<a href="https://www.sqlite.org/sqllogictest">sqllogic</a> '
"test set against sqlite and checks that all statements are passed",
lambda x: x.startswith("Sqllogic test"),
),
CheckDescription(
"SQLancer",
"Fuzzing tests that detect logical bugs with "
'<a href="https://github.com/sqlancer/sqlancer">SQLancer</a> tool',
lambda x: x.startswith("SQLancer"),
),
CheckDescription(
"Stateful tests",
"Runs stateful functional tests for ClickHouse binaries built in various "
"configurations -- release, debug, with sanitizers, etc",
lambda x: x.startswith("Stateful tests ("),
),
CheckDescription(
"Stateless tests",
"Runs stateless functional tests for ClickHouse binaries built in various "
"configurations -- release, debug, with sanitizers, etc",
lambda x: x.startswith("Stateless tests ("),
),
CheckDescription(
"Stress test",
"Runs stateless functional tests concurrently from several clients to detect "
"concurrency-related errors",
lambda x: x.startswith("Stress test ("),
),
CheckDescription(
JobNames.STYLE_CHECK,
"Runs a set of checks to keep the code style clean. If some of tests failed, "
"see the related log from the report",
lambda x: x == JobNames.STYLE_CHECK,
),
CheckDescription(
"Unit tests",
"Runs the unit tests for different release types",
lambda x: x.startswith("Unit tests ("),
),
CheckDescription(
"Upgrade check",
"Runs stress tests on server version from last release and then tries to "
"upgrade it to the version from the PR. It checks if the new server can "
"successfully startup without any errors, crashes or sanitizer asserts",
lambda x: x.startswith("Upgrade check ("),
),
CheckDescription(
"ClickBench",
"Runs [ClickBench](https://github.com/ClickHouse/ClickBench/) with instant-attach table",
lambda x: x.startswith("ClickBench"),
),
CheckDescription(
"Fallback for unknown",
"There's no description for the check yet, please add it to "
"tests/ci/ci_config.py:CHECK_DESCRIPTIONS",
lambda x: True,
),
]

View File

@ -3,7 +3,7 @@ from dataclasses import dataclass, asdict
from typing import Optional, List, Dict, Any, Iterable from typing import Optional, List, Dict, Any, Iterable
from ci_utils import normalize_string from ci_utils import normalize_string
from ci_config import CILabels, CI_CONFIG, JobConfig, JobNames from ci_config import CI
from git_helper import Runner as GitRunner, GIT_PREFIX from git_helper import Runner as GitRunner, GIT_PREFIX
from pr_info import PRInfo from pr_info import PRInfo
@ -80,7 +80,7 @@ class CiSettings:
if not res.ci_jobs: if not res.ci_jobs:
res.ci_jobs = [] res.ci_jobs = []
res.ci_jobs.append(match.removeprefix("job_")) res.ci_jobs.append(match.removeprefix("job_"))
elif match.startswith("ci_set_") and match in CILabels: elif match.startswith("ci_set_") and match in CI.Tags:
if not res.ci_sets: if not res.ci_sets:
res.ci_sets = [] res.ci_sets = []
res.ci_sets.append(match) res.ci_sets.append(match)
@ -97,15 +97,15 @@ class CiSettings:
res.exclude_keywords += [ res.exclude_keywords += [
normalize_string(keyword) for keyword in keywords normalize_string(keyword) for keyword in keywords
] ]
elif match == CILabels.NO_CI_CACHE: elif match == CI.Tags.NO_CI_CACHE:
res.no_ci_cache = True res.no_ci_cache = True
print("NOTE: CI Cache will be disabled") print("NOTE: CI Cache will be disabled")
elif match == CILabels.UPLOAD_ALL_ARTIFACTS: elif match == CI.Tags.UPLOAD_ALL_ARTIFACTS:
res.upload_all = True res.upload_all = True
print("NOTE: All binary artifacts will be uploaded") print("NOTE: All binary artifacts will be uploaded")
elif match == CILabels.DO_NOT_TEST_LABEL: elif match == CI.Tags.DO_NOT_TEST_LABEL:
res.do_not_test = True res.do_not_test = True
elif match == CILabels.NO_MERGE_COMMIT: elif match == CI.Tags.NO_MERGE_COMMIT:
res.no_merge_commit = True res.no_merge_commit = True
print("NOTE: Merge Commit will be disabled") print("NOTE: Merge Commit will be disabled")
elif match.startswith("batch_"): elif match.startswith("batch_"):
@ -131,18 +131,18 @@ class CiSettings:
def _check_if_selected( def _check_if_selected(
self, self,
job: str, job: str,
job_config: JobConfig, job_config: CI.JobConfig,
is_release: bool, is_release: bool,
is_pr: bool, is_pr: bool,
is_mq: bool, is_mq: bool,
labels: Iterable[str], labels: Iterable[str],
) -> bool: # type: ignore #too-many-return-statements ) -> bool: # type: ignore #too-many-return-statements
if self.do_not_test: if self.do_not_test:
label_config = CI_CONFIG.get_label_config(CILabels.DO_NOT_TEST_LABEL) label_config = CI.get_tag_config(CI.Tags.DO_NOT_TEST_LABEL)
assert label_config, f"Unknown tag [{CILabels.DO_NOT_TEST_LABEL}]" assert label_config, f"Unknown tag [{CI.Tags.DO_NOT_TEST_LABEL}]"
if job in label_config.run_jobs: if job in label_config.run_jobs:
print( print(
f"Job [{job}] present in CI set [{CILabels.DO_NOT_TEST_LABEL}] - pass" f"Job [{job}] present in CI set [{CI.Tags.DO_NOT_TEST_LABEL}] - pass"
) )
return True return True
return False return False
@ -164,7 +164,7 @@ class CiSettings:
to_deny = False to_deny = False
if self.include_keywords: if self.include_keywords:
if job == JobNames.STYLE_CHECK: if job == CI.JobNames.STYLE_CHECK:
# never exclude Style Check by include keywords # never exclude Style Check by include keywords
return True return True
for keyword in self.include_keywords: for keyword in self.include_keywords:
@ -175,7 +175,7 @@ class CiSettings:
if self.ci_sets: if self.ci_sets:
for tag in self.ci_sets: for tag in self.ci_sets:
label_config = CI_CONFIG.get_label_config(tag) label_config = CI.get_tag_config(tag)
assert label_config, f"Unknown tag [{tag}]" assert label_config, f"Unknown tag [{tag}]"
if job in label_config.run_jobs: if job in label_config.run_jobs:
print(f"Job [{job}] present in CI set [{tag}] - pass") print(f"Job [{job}] present in CI set [{tag}] - pass")
@ -197,12 +197,12 @@ class CiSettings:
def apply( def apply(
self, self,
job_configs: Dict[str, JobConfig], job_configs: Dict[str, CI.JobConfig],
is_release: bool, is_release: bool,
is_pr: bool, is_pr: bool,
is_mq: bool, is_mq: bool,
labels: Iterable[str], labels: Iterable[str],
) -> Dict[str, JobConfig]: ) -> Dict[str, CI.JobConfig]:
""" """
Apply CI settings from pr body Apply CI settings from pr body
""" """
@ -220,7 +220,7 @@ class CiSettings:
add_parents = [] add_parents = []
for job in list(res): for job in list(res):
parent_jobs = CI_CONFIG.get_job_parents(job) parent_jobs = CI.get_job_parents(job)
for parent_job in parent_jobs: for parent_job in parent_jobs:
if parent_job not in res: if parent_job not in res:
add_parents.append(parent_job) add_parents.append(parent_job)

View File

@ -17,7 +17,7 @@ from github.GithubObject import NotSet
from github.IssueComment import IssueComment from github.IssueComment import IssueComment
from github.Repository import Repository from github.Repository import Repository
from ci_config import CHECK_DESCRIPTIONS, CheckDescription, StatusNames, CIConfig from ci_config import CI
from env_helper import GITHUB_REPOSITORY, GITHUB_UPSTREAM_REPOSITORY, TEMP_PATH from env_helper import GITHUB_REPOSITORY, GITHUB_UPSTREAM_REPOSITORY, TEMP_PATH
from lambda_shared_package.lambda_shared.pr import Labels from lambda_shared_package.lambda_shared.pr import Labels
from pr_info import PRInfo from pr_info import PRInfo
@ -160,7 +160,7 @@ def set_status_comment(commit: Commit, pr_info: PRInfo) -> None:
if not statuses: if not statuses:
return return
if not [status for status in statuses if status.context == StatusNames.CI]: if not [status for status in statuses if status.context == CI.StatusNames.CI]:
# This is the case, when some statuses already exist for the check, # This is the case, when some statuses already exist for the check,
# but not the StatusNames.CI. We should create it as pending. # but not the StatusNames.CI. We should create it as pending.
# W/o pr_info to avoid recursion, and yes, one extra create_ci_report # W/o pr_info to avoid recursion, and yes, one extra create_ci_report
@ -169,7 +169,7 @@ def set_status_comment(commit: Commit, pr_info: PRInfo) -> None:
PENDING, PENDING,
create_ci_report(pr_info, statuses), create_ci_report(pr_info, statuses),
"The report for running CI", "The report for running CI",
StatusNames.CI, CI.StatusNames.CI,
) )
# We update the report in generate_status_comment function, so do it each # We update the report in generate_status_comment function, so do it each
@ -212,20 +212,20 @@ def generate_status_comment(pr_info: PRInfo, statuses: CommitStatuses) -> str:
f"\n" f"\n"
) )
# group checks by the name to get the worst one per each # group checks by the name to get the worst one per each
grouped_statuses = {} # type: Dict[CheckDescription, CommitStatuses] grouped_statuses = {} # type: Dict[CI.CheckDescription, CommitStatuses]
for status in statuses: for status in statuses:
cd = None cd = None
for c in CHECK_DESCRIPTIONS: for c in CI.CHECK_DESCRIPTIONS:
if c.match_func(status.context): if c.match_func(status.context):
cd = c cd = c
break break
if cd is None or cd == CHECK_DESCRIPTIONS[-1]: if cd is None or cd == CI.CHECK_DESCRIPTIONS[-1]:
# This is the case for either non-found description or a fallback # This is the case for either non-found description or a fallback
cd = CheckDescription( cd = CI.CheckDescription(
status.context, status.context,
CHECK_DESCRIPTIONS[-1].description, CI.CHECK_DESCRIPTIONS[-1].description,
CHECK_DESCRIPTIONS[-1].match_func, CI.CHECK_DESCRIPTIONS[-1].match_func,
) )
if cd in grouped_statuses: if cd in grouped_statuses:
@ -301,7 +301,7 @@ def create_ci_report(pr_info: PRInfo, statuses: CommitStatuses) -> str:
) )
) )
return upload_results( return upload_results(
S3Helper(), pr_info.number, pr_info.sha, test_results, [], StatusNames.CI S3Helper(), pr_info.number, pr_info.sha, test_results, [], CI.StatusNames.CI
) )
@ -435,7 +435,7 @@ def set_mergeable_check(
state, state,
report_url, report_url,
format_description(description), format_description(description),
StatusNames.MERGEABLE, CI.StatusNames.MERGEABLE,
) )
@ -443,7 +443,7 @@ def update_mergeable_check(commit: Commit, pr_info: PRInfo, check_name: str) ->
"check if the check_name in REQUIRED_CHECKS and then trigger update" "check if the check_name in REQUIRED_CHECKS and then trigger update"
not_run = ( not_run = (
pr_info.labels.intersection({Labels.SKIP_MERGEABLE_CHECK, Labels.RELEASE}) pr_info.labels.intersection({Labels.SKIP_MERGEABLE_CHECK, Labels.RELEASE})
or not CIConfig.is_required(check_name) or not CI.is_required(check_name)
or pr_info.release_pr or pr_info.release_pr
or pr_info.number == 0 or pr_info.number == 0
) )
@ -465,13 +465,11 @@ def trigger_mergeable_check(
workflow_failed: bool = False, workflow_failed: bool = False,
) -> StatusType: ) -> StatusType:
"""calculate and update StatusNames.MERGEABLE""" """calculate and update StatusNames.MERGEABLE"""
required_checks = [ required_checks = [status for status in statuses if CI.is_required(status.context)]
status for status in statuses if CIConfig.is_required(status.context)
]
mergeable_status = None mergeable_status = None
for status in statuses: for status in statuses:
if status.context == StatusNames.MERGEABLE: if status.context == CI.StatusNames.MERGEABLE:
mergeable_status = status mergeable_status = status
break break
@ -548,7 +546,7 @@ def update_upstream_sync_status(
"Using commit %s to post the %s status `%s`: [%s]", "Using commit %s to post the %s status `%s`: [%s]",
last_synced_upstream_commit.sha, last_synced_upstream_commit.sha,
sync_status, sync_status,
StatusNames.SYNC, CI.StatusNames.SYNC,
"", "",
) )
post_commit_status( post_commit_status(
@ -556,7 +554,7 @@ def update_upstream_sync_status(
sync_status, sync_status,
"", "",
"", "",
StatusNames.SYNC, CI.StatusNames.SYNC,
) )
trigger_mergeable_check( trigger_mergeable_check(
last_synced_upstream_commit, last_synced_upstream_commit,

View File

@ -9,10 +9,10 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Union
from sys import modules from sys import modules
from docker_images_helper import get_images_info from docker_images_helper import get_images_info
from ci_config import DigestConfig
from git_helper import Runner from git_helper import Runner
from env_helper import ROOT_DIR from env_helper import ROOT_DIR
from ci_utils import cd from ci_utils import cd
from ci_config import CI
DOCKER_DIGEST_LEN = 12 DOCKER_DIGEST_LEN = 12
JOB_DIGEST_LEN = 10 JOB_DIGEST_LEN = 10
@ -139,20 +139,21 @@ class DockerDigester:
class JobDigester: class JobDigester:
def __init__(self): def __init__(self, dry_run: bool = False):
self.dd = DockerDigester() self.dd = DockerDigester()
self.cache: Dict[str, str] = {} self.cache: Dict[str, str] = {}
self.dry_run = dry_run
@staticmethod @staticmethod
def _get_config_hash(digest_config: DigestConfig) -> str: def _get_config_hash(digest_config: CI.DigestConfig) -> str:
data_dict = asdict(digest_config) data_dict = asdict(digest_config)
hash_obj = md5() hash_obj = md5()
hash_obj.update(str(data_dict).encode()) hash_obj.update(str(data_dict).encode())
hash_string = hash_obj.hexdigest() hash_string = hash_obj.hexdigest()
return hash_string return hash_string
def get_job_digest(self, digest_config: DigestConfig) -> str: def get_job_digest(self, digest_config: CI.DigestConfig) -> str:
if not digest_config.include_paths: if not digest_config.include_paths or self.dry_run:
# job is not for digest # job is not for digest
return "f" * JOB_DIGEST_LEN return "f" * JOB_DIGEST_LEN

View File

@ -8,7 +8,7 @@ import logging
from pathlib import Path from pathlib import Path
from build_download_helper import download_build_with_progress from build_download_helper import download_build_with_progress
from ci_config import CI_CONFIG from ci_config import CI
from env_helper import RUNNER_TEMP, S3_ARTIFACT_DOWNLOAD_TEMPLATE from env_helper import RUNNER_TEMP, S3_ARTIFACT_DOWNLOAD_TEMPLATE
from git_helper import Git, commit from git_helper import Git, commit
from version_helper import get_version_from_repo, version_arg from version_helper import get_version_from_repo, version_arg
@ -59,7 +59,8 @@ def main():
temp_path.mkdir(parents=True, exist_ok=True) temp_path.mkdir(parents=True, exist_ok=True)
for build in args.build_names: for build in args.build_names:
# check if it's in CI_CONFIG # check if it's in CI_CONFIG
config = CI_CONFIG.build_config[build] config = CI.JOB_CONFIGS[build].build_config
assert config
if args.rename and config.static_binary_name: if args.rename and config.static_binary_name:
path = temp_path / f"clickhouse-{config.static_binary_name}" path = temp_path / f"clickhouse-{config.static_binary_name}"
else: else:

View File

@ -9,8 +9,9 @@ from build_download_helper import APIException, get_gh_api
module_dir = p.abspath(p.dirname(__file__)) module_dir = p.abspath(p.dirname(__file__))
git_root = p.abspath(p.join(module_dir, "..", "..")) git_root = p.abspath(p.join(module_dir, "..", ".."))
ROOT_DIR = git_root ROOT_DIR = git_root
CI = bool(os.getenv("CI")) IS_CI = bool(os.getenv("CI"))
TEMP_PATH = os.getenv("TEMP_PATH", p.abspath(p.join(module_dir, "./tmp"))) TEMP_PATH = os.getenv("TEMP_PATH", p.abspath(p.join(module_dir, "./tmp")))
REPORT_PATH = f"{TEMP_PATH}/reports" REPORT_PATH = f"{TEMP_PATH}/reports"
# FIXME: latest should not be used in CI, set temporary for transition to "docker with digest as a tag" # FIXME: latest should not be used in CI, set temporary for transition to "docker with digest as a tag"

View File

@ -4,7 +4,7 @@ import logging
from github import Github from github import Github
from ci_config import StatusNames from ci_config import CI
from commit_status_helper import ( from commit_status_helper import (
get_commit, get_commit,
get_commit_filtered_statuses, get_commit_filtered_statuses,
@ -71,7 +71,7 @@ def main():
can_set_green_mergeable_status=True, can_set_green_mergeable_status=True,
) )
ci_running_statuses = [s for s in statuses if s.context == StatusNames.CI] ci_running_statuses = [s for s in statuses if s.context == CI.StatusNames.CI]
if not ci_running_statuses: if not ci_running_statuses:
return return
# Take the latest status # Take the latest status
@ -81,7 +81,11 @@ def main():
has_pending = False has_pending = False
error_cnt = 0 error_cnt = 0
for status in statuses: for status in statuses:
if status.context in (StatusNames.MERGEABLE, StatusNames.CI, StatusNames.SYNC): if status.context in (
CI.StatusNames.MERGEABLE,
CI.StatusNames.CI,
CI.StatusNames.SYNC,
):
# do not account these statuses # do not account these statuses
continue continue
if status.state == PENDING: if status.state == PENDING:
@ -108,7 +112,7 @@ def main():
ci_state, ci_state,
ci_status.target_url, ci_status.target_url,
description, description,
StatusNames.CI, CI.StatusNames.CI,
pr_info, pr_info,
dump_to_file=True, dump_to_file=True,
) )

View File

@ -18,7 +18,7 @@ from collections import defaultdict
from itertools import chain from itertools import chain
from typing import Any, Dict from typing import Any, Dict
from env_helper import CI from env_helper import IS_CI
from integration_test_images import IMAGES from integration_test_images import IMAGES
MAX_RETRY = 1 MAX_RETRY = 1
@ -1004,7 +1004,7 @@ def run():
logging.info("Running tests") logging.info("Running tests")
if CI: if IS_CI:
# Avoid overlaps with previous runs # Avoid overlaps with previous runs
logging.info("Clearing dmesg before run") logging.info("Clearing dmesg before run")
subprocess.check_call("sudo -E dmesg --clear", shell=True) subprocess.check_call("sudo -E dmesg --clear", shell=True)
@ -1012,7 +1012,7 @@ def run():
state, description, test_results, _ = runner.run_impl(repo_path, build_path) state, description, test_results, _ = runner.run_impl(repo_path, build_path)
logging.info("Tests finished") logging.info("Tests finished")
if CI: if IS_CI:
# Dump dmesg (to capture possible OOMs) # Dump dmesg (to capture possible OOMs)
logging.info("Dumping dmesg") logging.info("Dumping dmesg")
subprocess.check_call("sudo -E dmesg -T", shell=True) subprocess.check_call("sudo -E dmesg -T", shell=True)

View File

@ -13,7 +13,6 @@ import requests
from build_download_helper import ( from build_download_helper import (
download_build_with_progress, download_build_with_progress,
get_build_name_for_check,
read_build_urls, read_build_urls,
) )
from compress_files import compress_fast from compress_files import compress_fast
@ -25,6 +24,7 @@ from report import FAILURE, SUCCESS, JobReport, TestResult, TestResults
from ssh import SSHKey from ssh import SSHKey
from stopwatch import Stopwatch from stopwatch import Stopwatch
from tee_popen import TeePopen from tee_popen import TeePopen
from ci_config import CI
JEPSEN_GROUP_NAME = "jepsen_group" JEPSEN_GROUP_NAME = "jepsen_group"
@ -224,7 +224,7 @@ def main():
head = requests.head(build_url, timeout=60) head = requests.head(build_url, timeout=60)
assert head.status_code == 200, f"Clickhouse binary not found: {build_url}" assert head.status_code == 200, f"Clickhouse binary not found: {build_url}"
else: else:
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
urls = read_build_urls(build_name, REPORT_PATH) urls = read_build_urls(build_name, REPORT_PATH)
build_url = None build_url = None
for url in urls: for url in urls:

View File

@ -12,7 +12,7 @@ from pathlib import Path
from github import Github from github import Github
from build_download_helper import download_builds_filter from build_download_helper import download_builds_filter
from ci_config import CI_CONFIG from ci_config import CI
from clickhouse_helper import get_instance_id, get_instance_type from clickhouse_helper import get_instance_id, get_instance_type
from commit_status_helper import get_commit from commit_status_helper import get_commit
from docker_images_helper import get_docker_image, pull_image from docker_images_helper import get_docker_image, pull_image
@ -83,7 +83,7 @@ def main():
assert ( assert (
check_name check_name
), "Check name must be provided as an input arg or in CHECK_NAME env" ), "Check name must be provided as an input arg or in CHECK_NAME env"
required_build = CI_CONFIG.test_configs[check_name].required_build required_build = CI.JOB_CONFIGS[check_name].get_required_build()
with open(GITHUB_EVENT_PATH, "r", encoding="utf-8") as event_file: with open(GITHUB_EVENT_PATH, "r", encoding="utf-8") as event_file:
event = json.load(event_file) event = json.load(event_file)

View File

@ -316,7 +316,9 @@ class PRInfo:
@property @property
def is_master(self) -> bool: def is_master(self) -> bool:
return self.number == 0 and self.head_ref == "master" return (
self.number == 0 and self.head_ref == "master" and not self.is_merge_queue
)
@property @property
def is_release(self) -> bool: def is_release(self) -> bool:
@ -324,7 +326,10 @@ class PRInfo:
@property @property
def is_pr(self): def is_pr(self):
return self.event_type == EventType.PULL_REQUEST if self.event_type == EventType.PULL_REQUEST:
assert self.number
return True
return False
@property @property
def is_scheduled(self) -> bool: def is_scheduled(self) -> bool:
@ -353,9 +358,6 @@ class PRInfo:
if self.changed_files_requested: if self.changed_files_requested:
return return
if not getattr(self, "diff_urls", False):
raise TypeError("The event does not have diff URLs")
for diff_url in self.diff_urls: for diff_url in self.diff_urls:
response = get_gh_api( response = get_gh_api(
diff_url, diff_url,

View File

@ -21,7 +21,7 @@ from typing import (
) )
from build_download_helper import get_gh_api from build_download_helper import get_gh_api
from ci_config import CI_CONFIG, BuildConfig from ci_config import CI
from ci_utils import normalize_string from ci_utils import normalize_string
from env_helper import REPORT_PATH, TEMP_PATH from env_helper import REPORT_PATH, TEMP_PATH
@ -412,6 +412,7 @@ class BuildResult:
ref_report = None ref_report = None
master_report = None master_report = None
any_report = None any_report = None
Path(REPORT_PATH).mkdir(parents=True, exist_ok=True)
for file in Path(REPORT_PATH).iterdir(): for file in Path(REPORT_PATH).iterdir():
if f"{build_name}.json" in file.name: if f"{build_name}.json" in file.name:
any_report = file any_report = file
@ -448,8 +449,10 @@ class BuildResult:
return json.dumps(asdict(self), indent=2) return json.dumps(asdict(self), indent=2)
@property @property
def build_config(self) -> Optional[BuildConfig]: def build_config(self) -> Optional[CI.BuildConfig]:
return CI_CONFIG.build_config.get(self.build_name, None) if self.build_name not in CI.JOB_CONFIGS:
return None
return CI.JOB_CONFIGS[self.build_name].build_config
@property @property
def comment(self) -> str: def comment(self) -> str:

View File

@ -5,7 +5,6 @@ from typing import Tuple
from github import Github from github import Github
from ci_config import StatusNames
from commit_status_helper import ( from commit_status_helper import (
create_ci_report, create_ci_report,
format_description, format_description,
@ -24,6 +23,7 @@ from lambda_shared_package.lambda_shared.pr import (
) )
from pr_info import PRInfo from pr_info import PRInfo
from report import FAILURE, PENDING, SUCCESS, StatusType from report import FAILURE, PENDING, SUCCESS, StatusType
from ci_config import CI
TRUSTED_ORG_IDS = { TRUSTED_ORG_IDS = {
54801242, # clickhouse 54801242, # clickhouse
@ -208,7 +208,7 @@ def main():
PENDING, PENDING,
ci_report_url, ci_report_url,
description, description,
StatusNames.CI, CI.StatusNames.CI,
pr_info, pr_info,
) )

View File

@ -11,7 +11,7 @@ import boto3 # type: ignore
import botocore # type: ignore import botocore # type: ignore
from compress_files import compress_file_fast from compress_files import compress_file_fast
from env_helper import ( from env_helper import (
CI, IS_CI,
RUNNER_TEMP, RUNNER_TEMP,
S3_BUILDS_BUCKET, S3_BUILDS_BUCKET,
S3_DOWNLOAD, S3_DOWNLOAD,
@ -111,13 +111,13 @@ class S3Helper:
self.client.delete_object(Bucket=bucket_name, Key=s3_path) self.client.delete_object(Bucket=bucket_name, Key=s3_path)
def upload_test_report_to_s3(self, file_path: Path, s3_path: str) -> str: def upload_test_report_to_s3(self, file_path: Path, s3_path: str) -> str:
if CI: if IS_CI:
return self._upload_file_to_s3(S3_TEST_REPORTS_BUCKET, file_path, s3_path) return self._upload_file_to_s3(S3_TEST_REPORTS_BUCKET, file_path, s3_path)
return S3Helper.copy_file_to_local(S3_TEST_REPORTS_BUCKET, file_path, s3_path) return S3Helper.copy_file_to_local(S3_TEST_REPORTS_BUCKET, file_path, s3_path)
def upload_build_file_to_s3(self, file_path: Path, s3_path: str) -> str: def upload_build_file_to_s3(self, file_path: Path, s3_path: str) -> str:
if CI: if IS_CI:
return self._upload_file_to_s3(S3_BUILDS_BUCKET, file_path, s3_path) return self._upload_file_to_s3(S3_BUILDS_BUCKET, file_path, s3_path)
return S3Helper.copy_file_to_local(S3_BUILDS_BUCKET, file_path, s3_path) return S3Helper.copy_file_to_local(S3_BUILDS_BUCKET, file_path, s3_path)
@ -255,7 +255,7 @@ class S3Helper:
if full_fs_path.is_symlink(): if full_fs_path.is_symlink():
if upload_symlinks: if upload_symlinks:
if CI: if IS_CI:
return self._upload_file_to_s3( return self._upload_file_to_s3(
bucket_name, bucket_name,
full_fs_path, full_fs_path,
@ -266,7 +266,7 @@ class S3Helper:
) )
return [] return []
if CI: if IS_CI:
return self._upload_file_to_s3( return self._upload_file_to_s3(
bucket_name, full_fs_path, full_s3_path + "/" + file_path.name bucket_name, full_fs_path, full_s3_path + "/" + file_path.name
) )
@ -331,7 +331,7 @@ class S3Helper:
return result return result
def url_if_exists(self, key: str, bucket: str = S3_BUILDS_BUCKET) -> str: def url_if_exists(self, key: str, bucket: str = S3_BUILDS_BUCKET) -> str:
if not CI: if not IS_CI:
local_path = self.local_path(bucket, key) local_path = self.local_path(bucket, key)
if local_path.exists(): if local_path.exists():
return local_path.as_uri() return local_path.as_uri()
@ -345,7 +345,7 @@ class S3Helper:
@staticmethod @staticmethod
def get_url(bucket: str, key: str) -> str: def get_url(bucket: str, key: str) -> str:
if CI: if IS_CI:
return S3Helper.s3_url(bucket, key) return S3Helper.s3_url(bucket, key)
return S3Helper.local_path(bucket, key).as_uri() return S3Helper.local_path(bucket, key).as_uri()

View File

@ -6,12 +6,13 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from build_download_helper import get_build_name_for_check, read_build_urls from build_download_helper import read_build_urls
from docker_images_helper import DockerImage, get_docker_image, pull_image from docker_images_helper import DockerImage, get_docker_image, pull_image
from env_helper import REPORT_PATH, TEMP_PATH from env_helper import REPORT_PATH, TEMP_PATH
from report import FAILURE, SUCCESS, JobReport, TestResult, TestResults from report import FAILURE, SUCCESS, JobReport, TestResult, TestResults
from stopwatch import Stopwatch from stopwatch import Stopwatch
from tee_popen import TeePopen from tee_popen import TeePopen
from ci_config import CI
IMAGE_NAME = "clickhouse/sqlancer-test" IMAGE_NAME = "clickhouse/sqlancer-test"
@ -43,7 +44,7 @@ def main():
docker_image = pull_image(get_docker_image(IMAGE_NAME)) docker_image = pull_image(get_docker_image(IMAGE_NAME))
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
urls = read_build_urls(build_name, reports_path) urls = read_build_urls(build_name, reports_path)
if not urls: if not urls:
raise ValueError("No build URLs found") raise ValueError("No build URLs found")

View File

@ -6,12 +6,13 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from build_download_helper import get_build_name_for_check, read_build_urls from build_download_helper import read_build_urls
from docker_images_helper import get_docker_image, pull_image from docker_images_helper import get_docker_image, pull_image
from env_helper import REPORT_PATH, TEMP_PATH from env_helper import REPORT_PATH, TEMP_PATH
from pr_info import PRInfo from pr_info import PRInfo
from report import SUCCESS, JobReport, TestResult from report import SUCCESS, JobReport, TestResult
from stopwatch import Stopwatch from stopwatch import Stopwatch
from ci_config import CI
IMAGE_NAME = "clickhouse/sqltest" IMAGE_NAME = "clickhouse/sqltest"
@ -49,7 +50,7 @@ def main():
docker_image = pull_image(get_docker_image(IMAGE_NAME)) docker_image = pull_image(get_docker_image(IMAGE_NAME))
build_name = get_build_name_for_check(check_name) build_name = CI.get_required_build_name(check_name)
print(build_name) print(build_name)
urls = read_build_urls(build_name, reports_path) urls = read_build_urls(build_name, reports_path)
if not urls: if not urls:

View File

@ -13,7 +13,7 @@ from typing import List, Tuple, Union
import magic import magic
from docker_images_helper import get_docker_image, pull_image from docker_images_helper import get_docker_image, pull_image
from env_helper import CI, REPO_COPY, TEMP_PATH from env_helper import IS_CI, REPO_COPY, TEMP_PATH
from git_helper import GIT_PREFIX, git_runner from git_helper import GIT_PREFIX, git_runner
from pr_info import PRInfo from pr_info import PRInfo
from report import ERROR, FAILURE, SUCCESS, JobReport, TestResults, read_test_results from report import ERROR, FAILURE, SUCCESS, JobReport, TestResults, read_test_results
@ -152,7 +152,7 @@ def main():
run_cpp_check = True run_cpp_check = True
run_shell_check = True run_shell_check = True
run_python_check = True run_python_check = True
if CI and pr_info.number > 0: if IS_CI and pr_info.number > 0:
pr_info.fetch_changed_files() pr_info.fetch_changed_files()
run_cpp_check = any( run_cpp_check = any(
not (is_python(file) or is_shell(file)) for file in pr_info.changed_files not (is_python(file) or is_shell(file)) for file in pr_info.changed_files

View File

@ -5,12 +5,12 @@
import argparse import argparse
import sys import sys
from ci_config import StatusNames
from commit_status_helper import get_commit, post_commit_status from commit_status_helper import get_commit, post_commit_status
from get_robot_token import get_best_robot_token from get_robot_token import get_best_robot_token
from github_helper import GitHub from github_helper import GitHub
from pr_info import PRInfo from pr_info import PRInfo
from report import SUCCESS from report import SUCCESS
from ci_config import CI
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
@ -75,7 +75,7 @@ def set_sync_status(gh, pr_info, sync_pr):
if sync_pr.mergeable_state == "clean": if sync_pr.mergeable_state == "clean":
print(f"Sync PR [{sync_pr.number}] is clean") print(f"Sync PR [{sync_pr.number}] is clean")
post_commit_status( post_commit_status(
get_commit(gh, pr_info.sha), SUCCESS, "", "", StatusNames.SYNC get_commit(gh, pr_info.sha), SUCCESS, "", "", CI.StatusNames.SYNC
) )
else: else:
print( print(

View File

@ -5,12 +5,12 @@ from pathlib import Path
import shutil import shutil
from typing import Dict, Set from typing import Dict, Set
import unittest import unittest
from ci_config import Build, JobNames
from s3_helper import S3Helper from s3_helper import S3Helper
from ci_cache import CiCache from ci_cache import CiCache
from digest_helper import JOB_DIGEST_LEN from digest_helper import JOB_DIGEST_LEN
from commit_status_helper import CommitStatusData from commit_status_helper import CommitStatusData
from env_helper import S3_BUILDS_BUCKET, TEMP_PATH from env_helper import S3_BUILDS_BUCKET, TEMP_PATH
from ci_config import CI
def _create_mock_digest_1(string): def _create_mock_digest_1(string):
@ -21,8 +21,8 @@ def _create_mock_digest_2(string):
return md5((string + "+nonce").encode("utf-8")).hexdigest()[:JOB_DIGEST_LEN] return md5((string + "+nonce").encode("utf-8")).hexdigest()[:JOB_DIGEST_LEN]
DIGESTS = {job: _create_mock_digest_1(job) for job in JobNames} DIGESTS = {job: _create_mock_digest_1(job) for job in CI.JobNames}
DIGESTS2 = {job: _create_mock_digest_2(job) for job in JobNames} DIGESTS2 = {job: _create_mock_digest_2(job) for job in CI.JobNames}
# pylint:disable=protected-access # pylint:disable=protected-access
@ -84,8 +84,10 @@ class TestCiCache(unittest.TestCase):
NUM_BATCHES = 10 NUM_BATCHES = 10
DOCS_JOBS_NUM = 1 DOCS_JOBS_NUM = 1
assert len(set(job for job in JobNames)) == len(list(job for job in JobNames)) assert len(set(job for job in CI.JobNames)) == len(
NONDOCS_JOBS_NUM = len(set(job for job in JobNames)) - DOCS_JOBS_NUM list(job for job in CI.JobNames)
)
NONDOCS_JOBS_NUM = len(set(job for job in CI.JobNames)) - DOCS_JOBS_NUM
PR_NUM = 123456 PR_NUM = 123456
status = CommitStatusData( status = CommitStatusData(
@ -97,13 +99,13 @@ class TestCiCache(unittest.TestCase):
) )
### add some pending statuses for two batches, non-release branch ### add some pending statuses for two batches, non-release branch
for job in JobNames: for job in CI.JobNames:
ci_cache.push_pending(job, [0, 1, 2], NUM_BATCHES, release_branch=False) ci_cache.push_pending(job, [0, 1, 2], NUM_BATCHES, release_branch=False)
ci_cache_2.push_pending(job, [0, 1, 2], NUM_BATCHES, release_branch=False) ci_cache_2.push_pending(job, [0, 1, 2], NUM_BATCHES, release_branch=False)
### add success status for 0 batch, non-release branch ### add success status for 0 batch, non-release branch
batch = 0 batch = 0
for job in JobNames: for job in CI.JobNames:
ci_cache.push_successful( ci_cache.push_successful(
job, batch, NUM_BATCHES, status, release_branch=False job, batch, NUM_BATCHES, status, release_branch=False
) )
@ -113,21 +115,17 @@ class TestCiCache(unittest.TestCase):
### add failed status for 2 batch, non-release branch ### add failed status for 2 batch, non-release branch
batch = 2 batch = 2
for job in JobNames: for job in CI.JobNames:
ci_cache.push_failed(job, batch, NUM_BATCHES, status, release_branch=False) ci_cache.push_failed(job, batch, NUM_BATCHES, status, release_branch=False)
ci_cache_2.push_failed( ci_cache_2.push_failed(
job, batch, NUM_BATCHES, status, release_branch=False job, batch, NUM_BATCHES, status, release_branch=False
) )
### check all expected directories were created on s3 mock ### check all expected directories were created on s3 mock
expected_build_path_1 = f"{CiCache.JobType.SRCS.value}-{_create_mock_digest_1(Build.PACKAGE_RELEASE)}" expected_build_path_1 = f"{CiCache.JobType.SRCS.value}-{_create_mock_digest_1(CI.BuildNames.PACKAGE_RELEASE)}"
expected_docs_path_1 = ( expected_docs_path_1 = f"{CiCache.JobType.DOCS.value}-{_create_mock_digest_1(CI.JobNames.DOCS_CHECK)}"
f"{CiCache.JobType.DOCS.value}-{_create_mock_digest_1(JobNames.DOCS_CHECK)}" expected_build_path_2 = f"{CiCache.JobType.SRCS.value}-{_create_mock_digest_2(CI.BuildNames.PACKAGE_RELEASE)}"
) expected_docs_path_2 = f"{CiCache.JobType.DOCS.value}-{_create_mock_digest_2(CI.JobNames.DOCS_CHECK)}"
expected_build_path_2 = f"{CiCache.JobType.SRCS.value}-{_create_mock_digest_2(Build.PACKAGE_RELEASE)}"
expected_docs_path_2 = (
f"{CiCache.JobType.DOCS.value}-{_create_mock_digest_2(JobNames.DOCS_CHECK)}"
)
self.assertCountEqual( self.assertCountEqual(
list(s3_mock.files_on_s3_paths.keys()), list(s3_mock.files_on_s3_paths.keys()),
[ [
@ -174,7 +172,7 @@ class TestCiCache(unittest.TestCase):
) )
### check statuses for all jobs in cache ### check statuses for all jobs in cache
for job in JobNames: for job in CI.JobNames:
self.assertEqual( self.assertEqual(
ci_cache.is_successful(job, 0, NUM_BATCHES, release_branch=False), True ci_cache.is_successful(job, 0, NUM_BATCHES, release_branch=False), True
) )
@ -212,7 +210,7 @@ class TestCiCache(unittest.TestCase):
assert status2 is None assert status2 is None
### add some more pending statuses for two batches and for a release branch ### add some more pending statuses for two batches and for a release branch
for job in JobNames: for job in CI.JobNames:
ci_cache.push_pending( ci_cache.push_pending(
job, batches=[0, 1], num_batches=NUM_BATCHES, release_branch=True job, batches=[0, 1], num_batches=NUM_BATCHES, release_branch=True
) )
@ -226,7 +224,7 @@ class TestCiCache(unittest.TestCase):
sha="deadbeaf2", sha="deadbeaf2",
pr_num=PR_NUM, pr_num=PR_NUM,
) )
for job in JobNames: for job in CI.JobNames:
ci_cache.push_successful(job, 0, NUM_BATCHES, status, release_branch=True) ci_cache.push_successful(job, 0, NUM_BATCHES, status, release_branch=True)
### check number of cache files is as expected ### check number of cache files is as expected
@ -249,7 +247,7 @@ class TestCiCache(unittest.TestCase):
) )
### check statuses ### check statuses
for job in JobNames: for job in CI.JobNames:
self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, False), True) self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, False), True)
self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, True), True) self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, True), True)
self.assertEqual(ci_cache.is_successful(job, 1, NUM_BATCHES, False), False) self.assertEqual(ci_cache.is_successful(job, 1, NUM_BATCHES, False), False)
@ -273,7 +271,7 @@ class TestCiCache(unittest.TestCase):
### create new cache object and verify the same checks ### create new cache object and verify the same checks
ci_cache = CiCache(s3_mock, DIGESTS) ci_cache = CiCache(s3_mock, DIGESTS)
for job in JobNames: for job in CI.JobNames:
self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, False), True) self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, False), True)
self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, True), True) self.assertEqual(ci_cache.is_successful(job, 0, NUM_BATCHES, True), True)
self.assertEqual(ci_cache.is_successful(job, 1, NUM_BATCHES, False), False) self.assertEqual(ci_cache.is_successful(job, 1, NUM_BATCHES, False), False)

View File

@ -1,30 +1,460 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import unittest import unittest
from ci_config import CIStages, JobNames, CI_CONFIG, Runners from ci_config import CI
import ci as CIPY
from ci_settings import CiSettings
from pr_info import PRInfo, EventType
from s3_helper import S3Helper
from ci_cache import CiCache
from ci_utils import normalize_string
_TEST_EVENT_JSON = {"dummy": "dummy"}
# pylint:disable=protected-access,union-attr
class TestCIConfig(unittest.TestCase): class TestCIConfig(unittest.TestCase):
def test_runner_config(self): def test_runner_config(self):
"""check runner is provided w/o exception""" """check runner is provided w/o exception"""
for job in JobNames: for job in CI.JobNames:
runner = CI_CONFIG.get_runner_type(job) self.assertIn(CI.JOB_CONFIGS[job].runner_type, CI.Runners)
self.assertIn(runner, Runners) if (
job
in (
CI.JobNames.STYLE_CHECK,
CI.JobNames.BUILD_CHECK,
)
or "jepsen" in job.lower()
):
self.assertTrue(
"style" in CI.JOB_CONFIGS[job].runner_type,
f"Job [{job}] must have style-checker(-aarch64) runner",
)
elif "binary_" in job.lower() or "package_" in job.lower():
self.assertTrue(
CI.JOB_CONFIGS[job].runner_type == CI.Runners.BUILDER,
f"Job [{job}] must have [{CI.Runners.BUILDER}] runner",
)
elif "aarch64" in job.lower():
self.assertTrue(
"aarch" in CI.JOB_CONFIGS[job].runner_type,
f"Job [{job}] does not match runner [{CI.JOB_CONFIGS[job].runner_type}]",
)
else:
self.assertTrue(
"aarch" not in CI.JOB_CONFIGS[job].runner_type,
f"Job [{job}] does not match runner [{CI.JOB_CONFIGS[job].runner_type}]",
)
def test_common_configs_applied_properly(self):
for job in CI.JobNames:
if CI.JOB_CONFIGS[job].job_name_keyword:
self.assertTrue(
CI.JOB_CONFIGS[job].job_name_keyword.lower()
in normalize_string(job),
f"Job [{job}] apparently uses wrong common config with job keyword [{CI.JOB_CONFIGS[job].job_name_keyword}]",
)
def test_required_checks(self):
for job in CI.REQUIRED_CHECKS:
if job in (CI.StatusNames.PR_CHECK, CI.StatusNames.SYNC):
continue
self.assertTrue(job in CI.JOB_CONFIGS, f"Job [{job}] not in job config")
def test_builds_configs(self):
"""build name in the build config must match the job name"""
for job in CI.JobNames:
self.assertTrue(job in CI.JOB_CONFIGS)
self.assertTrue(CI.JOB_CONFIGS[job].runner_type in CI.Runners)
if job in CI.BuildNames:
self.assertTrue(CI.JOB_CONFIGS[job].build_config.name == job)
self.assertTrue(CI.JOB_CONFIGS[job].required_builds is None)
else:
self.assertTrue(CI.JOB_CONFIGS[job].build_config is None)
if "asan" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_ASAN,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "msan" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_MSAN,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "tsan" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_TSAN,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "ubsan" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_UBSAN,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "debug" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_DEBUG,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "release" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
in (
CI.BuildNames.PACKAGE_RELEASE,
CI.BuildNames.BINARY_RELEASE,
),
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "coverage" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_RELEASE_COVERAGE,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "aarch" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_AARCH64,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "amd64" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_RELEASE,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "uzzer" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0] == CI.BuildNames.FUZZERS,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "Docker" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
in (
CI.BuildNames.PACKAGE_RELEASE,
CI.BuildNames.PACKAGE_AARCH64,
),
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "SQLTest" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
== CI.BuildNames.PACKAGE_RELEASE,
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif "Jepsen" in job:
self.assertTrue(
CI.JOB_CONFIGS[job].required_builds[0]
in (
CI.BuildNames.PACKAGE_RELEASE,
CI.BuildNames.BINARY_RELEASE,
),
f"Job [{job}] probably has wrong required build [{CI.JOB_CONFIGS[job].required_builds[0]}] in JobConfig",
)
elif job in (
CI.JobNames.STYLE_CHECK,
CI.JobNames.FAST_TEST,
CI.JobNames.BUILD_CHECK,
CI.JobNames.DOCS_CHECK,
CI.JobNames.BUGFIX_VALIDATE,
):
self.assertTrue(CI.JOB_CONFIGS[job].required_builds is None)
else:
print(f"Job [{job}] required build not checked")
def test_job_stage_config(self): def test_job_stage_config(self):
"""check runner is provided w/o exception""" """
for job in JobNames: check runner is provided w/o exception
stage = CI_CONFIG.get_job_ci_stage(job) """
if job in [ # check stages
JobNames.STYLE_CHECK, for job in CI.JobNames:
JobNames.FAST_TEST, if job in CI.BuildNames:
JobNames.JEPSEN_KEEPER, self.assertTrue(
JobNames.BUILD_CHECK, CI.get_job_ci_stage(job)
JobNames.BUILD_CHECK_SPECIAL, in (CI.WorkflowStages.BUILDS_1, CI.WorkflowStages.BUILDS_2)
]: )
assert (
stage == CIStages.NA
), "These jobs are not in CI stages, must be NA"
else: else:
assert stage != CIStages.NA, f"stage not found for [{job}]" if job in (
self.assertIn(stage, CIStages) CI.JobNames.STYLE_CHECK,
CI.JobNames.FAST_TEST,
CI.JobNames.JEPSEN_SERVER,
CI.JobNames.JEPSEN_KEEPER,
CI.JobNames.BUILD_CHECK,
):
self.assertEqual(
CI.get_job_ci_stage(job),
CI.WorkflowStages.NA,
msg=f"Stage for [{job}] is not correct",
)
else:
self.assertTrue(
CI.get_job_ci_stage(job)
in (CI.WorkflowStages.TESTS_1, CI.WorkflowStages.TESTS_3),
msg=f"Stage for [{job}] is not correct",
)
def test_build_jobs_configs(self):
"""
check build jobs have non-None build_config attribute
check test jobs have None build_config attribute
"""
for job in CI.JobNames:
if job in CI.BuildNames:
self.assertTrue(
isinstance(CI.JOB_CONFIGS[job].build_config, CI.BuildConfig)
)
else:
self.assertTrue(CI.JOB_CONFIGS[job].build_config is None)
def test_ci_py_for_pull_request(self):
"""
checks ci.py job configuration
"""
settings = CiSettings()
settings.no_ci_cache = True
settings.ci_sets = [CI.Tags.CI_SET_BUILDS]
settings.include_keywords = [
"package",
"integration",
"upgrade",
"clickHouse_build_check",
"stateless",
]
settings.exclude_keywords = ["asan", "aarch64"]
pr_info = PRInfo(github_event=_TEST_EVENT_JSON)
# make it pull request info
pr_info.event_type = EventType.PULL_REQUEST
pr_info.number = 12345
assert pr_info.is_pr and not pr_info.is_release and not pr_info.is_master
assert not pr_info.is_merge_queue
ci_cache = CIPY._configure_jobs(
S3Helper(), pr_info, settings, skip_jobs=False, dry_run=True
)
actual_jobs_to_do = list(ci_cache.jobs_to_do)
expected_jobs_to_do = []
for set_ in settings.ci_sets:
tag_config = CI.get_tag_config(set_)
assert tag_config
set_jobs = tag_config.run_jobs
for job in set_jobs:
if any(k in normalize_string(job) for k in settings.exclude_keywords):
continue
expected_jobs_to_do.append(job)
for job, config in CI.JOB_CONFIGS.items():
if not any(
keyword in normalize_string(job)
for keyword in settings.include_keywords
):
continue
if any(
keyword in normalize_string(job)
for keyword in settings.exclude_keywords
):
continue
if config.random_bucket:
continue
if job not in expected_jobs_to_do:
expected_jobs_to_do.append(job)
random_buckets = []
for job, config in ci_cache.jobs_to_do.items():
if config.random_bucket:
self.assertTrue(
config.random_bucket not in random_buckets,
"Only one job must be picked up from each random bucket",
)
random_buckets.append(config.random_bucket)
actual_jobs_to_do.remove(job)
self.assertCountEqual(expected_jobs_to_do, actual_jobs_to_do)
def test_ci_py_for_pull_request_no_settings(self):
"""
checks ci.py job configuration in PR with empty ci_settings
"""
settings = CiSettings()
settings.no_ci_cache = True
pr_info = PRInfo(github_event=_TEST_EVENT_JSON)
# make it pull request info
pr_info.event_type = EventType.PULL_REQUEST
pr_info.number = 12345
assert pr_info.is_pr and not pr_info.is_release and not pr_info.is_master
assert not pr_info.is_merge_queue
ci_cache = CIPY._configure_jobs(
S3Helper(), pr_info, settings, skip_jobs=False, dry_run=True
)
actual_jobs_to_do = list(ci_cache.jobs_to_do)
expected_jobs_to_do = []
for job, config in CI.JOB_CONFIGS.items():
if config.random_bucket:
continue
if config.release_only:
continue
if config.run_by_label:
continue
expected_jobs_to_do.append(job)
random_buckets = []
for job, config in ci_cache.jobs_to_do.items():
if config.random_bucket:
self.assertTrue(
config.random_bucket not in random_buckets,
"Only one job must be picked up from each random bucket",
)
random_buckets.append(config.random_bucket)
actual_jobs_to_do.remove(job)
self.assertCountEqual(expected_jobs_to_do, actual_jobs_to_do)
def test_ci_py_for_master(self):
"""
checks ci.py job configuration
"""
settings = CiSettings()
settings.no_ci_cache = True
pr_info = PRInfo(github_event=_TEST_EVENT_JSON)
pr_info.event_type = EventType.PUSH
assert pr_info.number == 0 and pr_info.is_release and not pr_info.is_merge_queue
ci_cache = CIPY._configure_jobs(
S3Helper(), pr_info, settings, skip_jobs=False, dry_run=True
)
actual_jobs_to_do = list(ci_cache.jobs_to_do)
expected_jobs_to_do = []
for job, config in CI.JOB_CONFIGS.items():
if config.pr_only:
continue
if config.run_by_label:
continue
if job in CI.MQ_JOBS:
continue
expected_jobs_to_do.append(job)
self.assertCountEqual(expected_jobs_to_do, actual_jobs_to_do)
def test_ci_py_for_merge_queue(self):
"""
checks ci.py job configuration
"""
settings = CiSettings()
settings.no_ci_cache = True
pr_info = PRInfo(github_event=_TEST_EVENT_JSON)
# make it merge_queue
pr_info.event_type = EventType.MERGE_QUEUE
assert (
pr_info.number == 0
and pr_info.is_merge_queue
and not pr_info.is_release
and not pr_info.is_master
and not pr_info.is_pr
)
ci_cache = CIPY._configure_jobs(
S3Helper(), pr_info, settings, skip_jobs=False, dry_run=True
)
actual_jobs_to_do = list(ci_cache.jobs_to_do)
expected_jobs_to_do = [
"Style check",
"Fast test",
"binary_release",
"Unit tests (release)",
]
self.assertCountEqual(expected_jobs_to_do, actual_jobs_to_do)
def test_ci_py_await(self):
"""
checks ci.py job configuration
"""
settings = CiSettings()
settings.no_ci_cache = True
pr_info = PRInfo(github_event=_TEST_EVENT_JSON)
pr_info.event_type = EventType.PUSH
pr_info.number = 0
assert pr_info.is_release and not pr_info.is_merge_queue
ci_cache = CIPY._configure_jobs(
S3Helper(), pr_info, settings, skip_jobs=False, dry_run=True
)
self.assertTrue(not ci_cache.jobs_to_skip, "Must be no jobs in skip list")
all_jobs_in_wf = list(ci_cache.jobs_to_do)
assert not ci_cache.jobs_to_wait
ci_cache.await_pending_jobs(is_release=pr_info.is_release, dry_run=True)
assert not ci_cache.jobs_to_skip
assert not ci_cache.jobs_to_wait
# pretend there are pending jobs that we neet to wait
ci_cache.jobs_to_wait = dict(ci_cache.jobs_to_do)
for job, config in ci_cache.jobs_to_wait.items():
assert not config.pending_batches
assert config.batches
config.pending_batches = list(config.batches)
for job, config in ci_cache.jobs_to_wait.items():
for batch in range(config.num_batches):
record = CiCache.Record(
record_type=CiCache.RecordType.PENDING,
job_name=job,
job_digest=ci_cache.job_digests[job],
batch=batch,
num_batches=config.num_batches,
release_branch=True,
)
for record_t_, records_ in ci_cache.records.items():
if record_t_.value == CiCache.RecordType.PENDING.value:
records_[record.to_str_key()] = record
def _test_await_for_batch(
ci_cache: CiCache, record_type: CiCache.RecordType, batch: int
) -> None:
assert ci_cache.jobs_to_wait
for job_, config_ in ci_cache.jobs_to_wait.items():
record = CiCache.Record(
record_type=record_type,
job_name=job_,
job_digest=ci_cache.job_digests[job_],
batch=batch,
num_batches=config_.num_batches,
release_branch=True,
)
for record_t_, records_ in ci_cache.records.items():
if record_t_.value == record_type.value:
records_[record.to_str_key()] = record
# await
ci_cache.await_pending_jobs(is_release=pr_info.is_release, dry_run=True)
for _, config_ in ci_cache.jobs_to_wait.items():
assert config_.pending_batches
if (
record_type != CiCache.RecordType.PENDING
and batch < config_.num_batches
):
assert batch not in config_.pending_batches
else:
assert batch in config_.pending_batches
for _, config_ in ci_cache.jobs_to_do.items():
# jobs to do must have batches to run before/after await
# if it's an empty list after await - apparently job has not been removed after await
assert config_.batches
_test_await_for_batch(ci_cache, CiCache.RecordType.SUCCESSFUL, 0)
# check all one-batch jobs are in jobs_to_skip
for job in all_jobs_in_wf:
config = CI.JOB_CONFIGS[job]
if config.num_batches == 1:
self.assertTrue(job in ci_cache.jobs_to_skip)
self.assertTrue(job not in ci_cache.jobs_to_do)
else:
self.assertTrue(job not in ci_cache.jobs_to_skip)
self.assertTrue(job in ci_cache.jobs_to_do)
_test_await_for_batch(ci_cache, CiCache.RecordType.FAILED, 1)
_test_await_for_batch(ci_cache, CiCache.RecordType.SUCCESSFUL, 2)
self.assertTrue(len(ci_cache.jobs_to_skip) > 0)
self.assertTrue(len(ci_cache.jobs_to_do) > 0)
self.assertCountEqual(
list(ci_cache.jobs_to_do) + ci_cache.jobs_to_skip, all_jobs_in_wf
)

View File

@ -4,7 +4,7 @@
import unittest import unittest
from ci_settings import CiSettings from ci_settings import CiSettings
from ci_config import JobConfig from ci_config import CI
_TEST_BODY_1 = """ _TEST_BODY_1 = """
#### Run only: #### Run only:
@ -64,8 +64,8 @@ _TEST_JOB_LIST = [
"fuzzers", "fuzzers",
"Docker server image", "Docker server image",
"Docker keeper image", "Docker keeper image",
"Install packages (amd64)", "Install packages (release)",
"Install packages (arm64)", "Install packages (aarch64)",
"Stateless tests (debug)", "Stateless tests (debug)",
"Stateless tests (release)", "Stateless tests (release)",
"Stateless tests (coverage)", "Stateless tests (coverage)",
@ -120,15 +120,15 @@ _TEST_JOB_LIST = [
"AST fuzzer (ubsan)", "AST fuzzer (ubsan)",
"ClickHouse Keeper Jepsen", "ClickHouse Keeper Jepsen",
"ClickHouse Server Jepsen", "ClickHouse Server Jepsen",
"Performance Comparison", "Performance Comparison (release)",
"Performance Comparison Aarch64", "Performance Comparison (aarch64)",
"Sqllogic test (release)", "Sqllogic test (release)",
"SQLancer (release)", "SQLancer (release)",
"SQLancer (debug)", "SQLancer (debug)",
"SQLTest", "SQLTest",
"Compatibility check (amd64)", "Compatibility check (release)",
"Compatibility check (aarch64)", "Compatibility check (aarch64)",
"ClickBench (amd64)", "ClickBench (release)",
"ClickBench (aarch64)", "ClickBench (aarch64)",
"libFuzzer tests", "libFuzzer tests",
"ClickHouse build check", "ClickHouse build check",
@ -166,7 +166,10 @@ class TestCIOptions(unittest.TestCase):
["tsan", "foobar", "aarch64", "analyzer", "s3_storage", "coverage"], ["tsan", "foobar", "aarch64", "analyzer", "s3_storage", "coverage"],
) )
jobs_configs = {job: JobConfig() for job in _TEST_JOB_LIST} jobs_configs = {
job: CI.JobConfig(runner_type=CI.Runners.STYLE_CHECKER)
for job in _TEST_JOB_LIST
}
jobs_configs[ jobs_configs[
"fuzzers" "fuzzers"
].run_by_label = ( ].run_by_label = (
@ -210,7 +213,10 @@ class TestCIOptions(unittest.TestCase):
) )
def test_options_applied_2(self): def test_options_applied_2(self):
jobs_configs = {job: JobConfig() for job in _TEST_JOB_LIST_2} jobs_configs = {
job: CI.JobConfig(runner_type=CI.Runners.STYLE_CHECKER)
for job in _TEST_JOB_LIST_2
}
jobs_configs["Style check"].release_only = True jobs_configs["Style check"].release_only = True
jobs_configs["Fast test"].pr_only = True jobs_configs["Fast test"].pr_only = True
jobs_configs["fuzzers"].run_by_label = "TEST_LABEL" jobs_configs["fuzzers"].run_by_label = "TEST_LABEL"
@ -252,7 +258,10 @@ class TestCIOptions(unittest.TestCase):
def test_options_applied_3(self): def test_options_applied_3(self):
ci_settings = CiSettings() ci_settings = CiSettings()
ci_settings.include_keywords = ["Style"] ci_settings.include_keywords = ["Style"]
jobs_configs = {job: JobConfig() for job in _TEST_JOB_LIST_2} jobs_configs = {
job: CI.JobConfig(runner_type=CI.Runners.STYLE_CHECKER)
for job in _TEST_JOB_LIST_2
}
jobs_configs["Style check"].release_only = True jobs_configs["Style check"].release_only = True
jobs_configs["Fast test"].pr_only = True jobs_configs["Fast test"].pr_only = True
# no settings are set # no settings are set
@ -296,7 +305,10 @@ class TestCIOptions(unittest.TestCase):
) )
self.assertCountEqual(ci_options.include_keywords, ["analyzer"]) self.assertCountEqual(ci_options.include_keywords, ["analyzer"])
self.assertIsNone(ci_options.exclude_keywords) self.assertIsNone(ci_options.exclude_keywords)
jobs_configs = {job: JobConfig() for job in _TEST_JOB_LIST} jobs_configs = {
job: CI.JobConfig(runner_type=CI.Runners.STYLE_CHECKER)
for job in _TEST_JOB_LIST
}
jobs_configs[ jobs_configs[
"fuzzers" "fuzzers"
].run_by_label = "TEST_LABEL" # check "fuzzers" does not appears in the result ].run_by_label = "TEST_LABEL" # check "fuzzers" does not appears in the result

View File

@ -512,6 +512,7 @@ def test_when_s3_connection_reset_by_peer_at_create_mpu_retried(
), error ), error
@pytest.mark.skip(reason="test is flaky, waiting ClickHouse/issues/64451")
def test_query_is_canceled_with_inf_retries(cluster, broken_s3): def test_query_is_canceled_with_inf_retries(cluster, broken_s3):
node = cluster.instances["node_with_inf_s3_retries"] node = cluster.instances["node_with_inf_s3_retries"]

View File

@ -0,0 +1,3 @@
<clickhouse>
<async_load_databases>false</async_load_databases>
</clickhouse>

View File

@ -11,13 +11,13 @@ def cluster():
cluster = ClickHouseCluster(__file__) cluster = ClickHouseCluster(__file__)
cluster.add_instance( cluster.add_instance(
"node1", "node1",
main_configs=["configs/storage_conf.xml"], main_configs=["configs/storage_conf.xml", "configs/no_async_load.xml"],
with_nginx=True, with_nginx=True,
use_old_analyzer=True, use_old_analyzer=True,
) )
cluster.add_instance( cluster.add_instance(
"node2", "node2",
main_configs=["configs/storage_conf_web.xml"], main_configs=["configs/storage_conf_web.xml", "configs/no_async_load.xml"],
with_nginx=True, with_nginx=True,
stay_alive=True, stay_alive=True,
with_zookeeper=True, with_zookeeper=True,
@ -25,7 +25,7 @@ def cluster():
) )
cluster.add_instance( cluster.add_instance(
"node3", "node3",
main_configs=["configs/storage_conf_web.xml"], main_configs=["configs/storage_conf_web.xml", "configs/no_async_load.xml"],
with_nginx=True, with_nginx=True,
with_zookeeper=True, with_zookeeper=True,
use_old_analyzer=True, use_old_analyzer=True,
@ -33,7 +33,7 @@ def cluster():
cluster.add_instance( cluster.add_instance(
"node4", "node4",
main_configs=["configs/storage_conf.xml"], main_configs=["configs/storage_conf.xml", "configs/no_async_load.xml"],
with_nginx=True, with_nginx=True,
stay_alive=True, stay_alive=True,
with_installed_binary=True, with_installed_binary=True,
@ -42,7 +42,7 @@ def cluster():
) )
cluster.add_instance( cluster.add_instance(
"node5", "node5",
main_configs=["configs/storage_conf.xml"], main_configs=["configs/storage_conf.xml", "configs/no_async_load.xml"],
with_nginx=True, with_nginx=True,
use_old_analyzer=True, use_old_analyzer=True,
) )

View File

@ -88,6 +88,11 @@ def test_dynamic_query_handler():
"application/whatever; charset=cp1337" "application/whatever; charset=cp1337"
== res_custom_ct.headers["content-type"] == res_custom_ct.headers["content-type"]
) )
assert "it works" == res_custom_ct.headers["X-Test-Http-Response-Headers-Works"]
assert (
"also works"
== res_custom_ct.headers["X-Test-Http-Response-Headers-Even-Multiple"]
)
def test_predefined_query_handler(): def test_predefined_query_handler():
@ -146,6 +151,10 @@ def test_predefined_query_handler():
) )
assert b"max_final_threads\t1\nmax_threads\t1\n" == res2.content assert b"max_final_threads\t1\nmax_threads\t1\n" == res2.content
assert "application/generic+one" == res2.headers["content-type"] assert "application/generic+one" == res2.headers["content-type"]
assert "it works" == res2.headers["X-Test-Http-Response-Headers-Works"]
assert (
"also works" == res2.headers["X-Test-Http-Response-Headers-Even-Multiple"]
)
cluster.instance.query( cluster.instance.query(
"CREATE TABLE test_table (id UInt32, data String) Engine=TinyLog" "CREATE TABLE test_table (id UInt32, data String) Engine=TinyLog"
@ -212,6 +221,18 @@ def test_fixed_static_handler():
"test_get_fixed_static_handler", method="GET", headers={"XXX": "xxx"} "test_get_fixed_static_handler", method="GET", headers={"XXX": "xxx"}
).content ).content
) )
assert (
"it works"
== cluster.instance.http_request(
"test_get_fixed_static_handler", method="GET", headers={"XXX": "xxx"}
).headers["X-Test-Http-Response-Headers-Works"]
)
assert (
"also works"
== cluster.instance.http_request(
"test_get_fixed_static_handler", method="GET", headers={"XXX": "xxx"}
).headers["X-Test-Http-Response-Headers-Even-Multiple"]
)
def test_config_static_handler(): def test_config_static_handler():

View File

@ -18,6 +18,10 @@
<type>dynamic_query_handler</type> <type>dynamic_query_handler</type>
<query_param_name>get_dynamic_handler_query</query_param_name> <query_param_name>get_dynamic_handler_query</query_param_name>
<content_type>application/whatever; charset=cp1337</content_type> <content_type>application/whatever; charset=cp1337</content_type>
<http_response_headers>
<X-Test-Http-Response-Headers-Works>it works</X-Test-Http-Response-Headers-Works>
<X-Test-Http-Response-Headers-Even-Multiple>also works</X-Test-Http-Response-Headers-Even-Multiple>
</http_response_headers>
</handler> </handler>
</rule> </rule>
</http_handlers> </http_handlers>

View File

@ -19,6 +19,10 @@
<type>predefined_query_handler</type> <type>predefined_query_handler</type>
<query>SELECT name, value FROM system.settings WHERE name = {setting_name_1:String} OR name = {setting_name_2:String}</query> <query>SELECT name, value FROM system.settings WHERE name = {setting_name_1:String} OR name = {setting_name_2:String}</query>
<content_type>application/generic+one</content_type> <content_type>application/generic+one</content_type>
<http_response_headers>
<X-Test-Http-Response-Headers-Works>it works</X-Test-Http-Response-Headers-Works>
<X-Test-Http-Response-Headers-Even-Multiple>also works</X-Test-Http-Response-Headers-Even-Multiple>
</http_response_headers>
</handler> </handler>
</rule> </rule>
<rule> <rule>

View File

@ -12,6 +12,10 @@
<status>402</status> <status>402</status>
<content_type>text/html; charset=UTF-8</content_type> <content_type>text/html; charset=UTF-8</content_type>
<response_content>Test get static handler and fix content</response_content> <response_content>Test get static handler and fix content</response_content>
<http_response_headers>
<X-Test-Http-Response-Headers-Works>it works</X-Test-Http-Response-Headers-Works>
<X-Test-Http-Response-Headers-Even-Multiple>also works</X-Test-Http-Response-Headers-Even-Multiple>
</http_response_headers>
</handler> </handler>
</rule> </rule>

View File

@ -0,0 +1,3 @@
<clickhouse>
<async_load_databases>false</async_load_databases>
</clickhouse>

View File

@ -24,7 +24,7 @@ mysql8_node = None
node_db = cluster.add_instance( node_db = cluster.add_instance(
"node1", "node1",
main_configs=["configs/timezone_config.xml"], main_configs=["configs/timezone_config.xml", "configs/no_async_load.xml"],
user_configs=["configs/users.xml"], user_configs=["configs/users.xml"],
with_mysql57=True, with_mysql57=True,
with_mysql8=True, with_mysql8=True,
@ -32,7 +32,7 @@ node_db = cluster.add_instance(
) )
node_disable_bytes_settings = cluster.add_instance( node_disable_bytes_settings = cluster.add_instance(
"node2", "node2",
main_configs=["configs/timezone_config.xml"], main_configs=["configs/timezone_config.xml", "configs/no_async_load.xml"],
user_configs=["configs/users_disable_bytes_settings.xml"], user_configs=["configs/users_disable_bytes_settings.xml"],
with_mysql57=False, with_mysql57=False,
with_mysql8=False, with_mysql8=False,
@ -40,7 +40,7 @@ node_disable_bytes_settings = cluster.add_instance(
) )
node_disable_rows_settings = cluster.add_instance( node_disable_rows_settings = cluster.add_instance(
"node3", "node3",
main_configs=["configs/timezone_config.xml"], main_configs=["configs/timezone_config.xml", "configs/no_async_load.xml"],
user_configs=["configs/users_disable_rows_settings.xml"], user_configs=["configs/users_disable_rows_settings.xml"],
with_mysql57=False, with_mysql57=False,
with_mysql8=False, with_mysql8=False,

View File

@ -6,4 +6,5 @@
</merge_tree> </merge_tree>
<max_database_replicated_create_table_thread_pool_size>50</max_database_replicated_create_table_thread_pool_size> <max_database_replicated_create_table_thread_pool_size>50</max_database_replicated_create_table_thread_pool_size>
<allow_experimental_transactions>42</allow_experimental_transactions> <allow_experimental_transactions>42</allow_experimental_transactions>
<async_load_databases>false</async_load_databases>
</clickhouse> </clickhouse>

View File

@ -7,4 +7,5 @@
<max_database_replicated_create_table_thread_pool_size>50</max_database_replicated_create_table_thread_pool_size> <max_database_replicated_create_table_thread_pool_size>50</max_database_replicated_create_table_thread_pool_size>
<allow_experimental_transactions>42</allow_experimental_transactions> <allow_experimental_transactions>42</allow_experimental_transactions>
<replica_group_name>group</replica_group_name> <replica_group_name>group</replica_group_name>
<async_load_databases>false</async_load_databases>
</clickhouse> </clickhouse>

View File

@ -0,0 +1,3 @@
<clickhouse>
<!-- Will be overwritten by the test -->
</clickhouse>

Some files were not shown because too many files have changed in this diff Show More