Commit 4ef0bb3
PS-9148 feature: Add caching of dictionary table for component_masking_functions
https://perconadev.atlassian.net/browse/PS-9148
- Added caching of mysql.masking_dictionaries table content.
- Implemented masking_dictionaries_flush() UDF which flushes data
from the masking dictionaries table to the memory cache.
PS-9148 feature: Add masking_functions.masking_database sys var support
https://perconadev.atlassian.net/browse/PS-9148
The masking_functions.masking_database system variable for the
masking_functions component specifies database used for data
masking dictionaries.
PS-9148 feature: Implement dictionary flusher for masking_functions plugin
https://perconadev.atlassian.net/browse/PS-9148
- Added component_masking.dictionaries_flush_interval_seconds system
variable.
- Added actual flusher thread. It periodically rereads content of
dictionary table and updates in-memory cache.
PS-9148 feature: Implemented hierarchical storage for dictionaries and terms
https://perconadev.atlassian.net/browse/PS-9148
Introduced 'dictionary' and 'bookshelf' classes for storing terms on
per-dictionary level.
Reworked 'query_cache' to utilize these two new classes.
PS-9148 feature: Minor refactoring to break dependencies
https://perconadev.atlassian.net/browse/PS-9148
Introduced 'component_sys_variable_service_tuple' class for groupping comonent
system variable registration services (supposed to be used with
'primitive_singleton' class template).
'query_cache' now expects 'query_builder' and 'flusher_interval_seconds' as its
constructor's parameters.
Eliminates custom MySQL types (like 'ulonglong') and its includes (like
'my_inttypes.h') from the publicly facing headers.
'query_cache' is now explicitly initialized / deinitialized in the component's
'init()'' / 'deinit()'' functions via 'primitive_singleton' interface.
'query_cache' helper thread-related methods made private.
PS-9148 feature: Refactored usage of std::string_view for c-interfaces
https://perconadev.atlassian.net/browse/PS-9148
As std::string_view::data() is not guaranteed to be null-terminated, it is
not safe to use it in old c-functions accepting 'const char *'.
Some constants converted to arrays of char 'const char buffer[]{"value"}'.
PS-9148 feature: Implemented lazy query_cache initial population
https://perconadev.atlassian.net/browse/PS-9148
'command_service_tuple' struct extended with one more member - 'field_info'
service.
Reworked 'query_cache' class: instead of loading terms from the database in
constructor, this operation is now performed in first attempt to access one
of the dictionary methods ('contains()' / 'get_random()' / 'remove()' /
'insert()'). This is done in order to overcome a limitation that does not
allow 'mysql_command_query' service to be used from inside the componment
initialization function.
Fixed problem with 'm_dict_cache' shared pointer updated concurrently from
different threads.
Exceptions thrown from the cache loading function no longer escape the
flusher thread.
De-coupled 'sql_context' and 'bookshelf' classes: 'sql_context' now accepts a
generic insertion callback that can be used to populate any type of containers.
'component_masking_functions.dictionary_operations' MTR test case extended with
additional checks for flushed / unflushed dictionary cache.
PS-9148 feature: Reworked dictionary / bookshelf thread-safety model
https://perconadev.atlassian.net/browse/PS-9148
Both 'dictionary' and 'bookshelf' classes no longer include their own
'std::shared_mutex' to protect data. Instead, we now have a single
'std::shared_mutex' at the 'query_cache' level.
The return value of the 'get_random()' method in both 'dictionary' and
'bookshelf' classes changed from 'optional_string' to 'std::string_view'. Empty
(default constructed) 'std::string_view' is used as an indicator of an
unsuccessful operation.
'get_random()' method in the 'query_cache' class still returns a string by
value to avoid race conditions.
Changed the behaviour of the 'sql_context::execute_dml()' method - it now
throws when SQL errors (like "no table found", etc.) occur.
PS-9148 feature: Fix masking functions flusher thread intialization
https://perconadev.atlassian.net/browse/PS-9148
Added missing my_thread_attr_t initialization.
PS-9148 feature: Decoupled threading and caching functionality in query_cache
https://perconadev.atlassian.net/browse/PS-9148
Threading-related functionality extracted from the 'query_cache' class into
a separate 'dictionary_flusher_thread' class.
This new class now accepts an instance of existing 'query_cache' class as
a parameter of its constructor in a form of shared pointer.
Changed the way how these two objects are now initialized / deinitialized in
'component.cpp' ('component_init()' / 'component_deinit()' functions).
PS-9148 feature: Refactored dictionary_flusher_thread
https://perconadev.atlassian.net/browse/PS-9148
'dictionary_flusher_thread' class interface ('dictionary_flusher_thread.hpp') now
includes no public / internal MySQL headers.
Introduced internal 'thread_handler_context' class, which is supposed to be
instantiated as the very first declaration of the MySQL thread handler function -
it performs proper 'THD' object initialization in constructor and
deinitialization in destructor.
Introduced internal 'thread_attributes' class - an RAII wrapper over
'my_thread_attr_t' with proper initialization in constructor and deinitialization
in destructor.
Introduced internal 'jthread' class that similarly to 'std::jthread' from c++20
spawns a joinable thread in constructor and joins it in destructor. It expects
only meaningful logic in a form of 'std::function<void()>' from the user and
hides all the MySQL initialization / PSI registration boilerplate. It uses
an instance of 'thread_attributes' class when spawns a thread. It also creates
an instance of the 'thread_handler_context' class inside actual thread handler
function ('jthread::raw_handler()'). This class also makes sure that no
exception escapes actual thread handler function.
Refactored error handling in 'component_init()'.
Fixed problem with updating 'stopped_' variable (which the condition variable
uses in its 'waitt_for()' method) without properly locking the mutex.
Fixed instabilities in 'component_masking_functions.rpl_dictionaries_flush_interval'
MTR test case.
PS-9148 feature: Improved diagnostics (MySQL API error messages) in sql_context
https://perconadev.atlassian.net/browse/PS-9148
'command_service_tuple' class extended with new 'error_info' member of type
'SERVICE_TYPE(mysql_command_error_info) *' that allows extracting MySQL
error codes and messages. It is expected to be used inside 'sql_context' class
methods.
Reworked the way how exceptions are thrown from the 'sql_context' class
methods - we now use 'raise_with_error_message()' helper method that
throws an exception that incorporates MySQL client API error message
extracted via added 'error_info' member of the `command_service_tuple`
class.
More meaningful error messages, which include underlying MySQL error
descriptions, are now generated from inside the 'query_cache' class methods.
Added new DBUG keywords and DEBUG_SYNC actions that allow control
over Dictionary Flusher background thread actions.
Added new 'component_masking_functions.flusher_thread_suspend_resume'
MTR test case that checks for various race conditions between current session
and Dictionary Flusher background thread.
Improved 'component_masking_functions.rpl_dictionaries_flush_interval' MTR
test case - pre-recorder values replaced with 'assert.inc'.
PS-9148 feature: Refactored to avoid deadlock during UNINSTALL COMPONENT
https://perconadev.atlassian.net/browse/PS-9148
'sql_context' class constructor now takes one extra parameter that allows
to specify whether the user wants to set the
'MYSQL_NO_LOCK_REGISTRY' option or not.
Introduced new abstract class 'basic_sql_context_builder' that is used to
construct instances of the 'sql_context' class on demand.
Also added two concrete implementations of this interface:
'default_sql_context_builder' and 'static_sql_context_builder'.
The former just creates a new instance of the 'sql_context' class every time
the 'build()' method is invoked. This implementation is used from inside
the UDF function implementation methods.
The latter creates an instance of the 'sql_context' class during the first call
to the 'build()' method, saves it internally and returns this saved instance
for each subsequent call to the 'build()' method. This implementation is
used in the 'dictionary_flusher_thread'.
Refactored 'query_cache' class: basic functionality that needs external
'basic_sql_context_builder' and 'query_builder' extracted into separate
class 'sql_cache_core'.
For convenience, added new 'sql_cache' class as a wrapper over existing
'sql_cache_code', 'basic_sql_context_builder' and 'query_builder'. The
same 'sql_cache_core' can be shared between multiple instances of
the `sql_cache`.
This allowed to make sure that `dictionary_flusher_thread` uses a
dedicated long-living instance of the 'sql_context' class (with
'MYSQL_NO_LOCK_REGISTRY' enabled) and does not cause deadlocks
during 'UNINSTALL COMPONENT'.
See Bug #34741098 "component::deinit() will block if calling any registry"
(commit mysql/mysql-server@d39330f)
for more details.
As the 'sql_context' connection in the 'dictionary_flusher_thread' is now
a long-living one, it is now shown in the output of the 'SHOW PROCESSLIST'
statement.
Modified 'count_sessions.inc' / 'wait_until_count_sessions.inc' logic inside
'component_masking_functions.flusher_thread_suspend_resume' MTR
test case to reflect these changes.
PS-9148 feature: Refactored dictionary flusher thread startup / termination
https://perconadev.atlassian.net/browse/PS-9148
'command_service_tuple' extended with one more member 'thread' which is used
to initialize / deinitialize threads intended to be used as MySQL threads.
'sql_context' class constructor now takes one extra parameter that allows to
specify whether the user wants to associate a new session (including an instance
of the 'THD' class) with the calling thread. Internally it is done by setting the
'MYSQL_COMMAND_LOCAL_THD_HANDLE' option to nullptr.
Also 'sql_context' now tries to open connections on behalf of the internal
predefined 'mysql.session' MySQL user (instead of 'root').
Reworked 'static_sql_context_builder' class - it now creates a shared "static"
instance of he 'sql_context' class inside the class constructor and passes true
as its 'initialize_thread' parameter meaning an intent to associate the calling
thread with this connection. Before this change, the construction was done inside
the very first call to 'do_build()'.
The regular ("new instance per request" ) implementation of the
'basic_sql_context_builder', 'default_sql_context_builder', now passes false as
the 'initialize_thread' parameter (meaning no association with the thread needed).
Significantly reworked 'dictionary_flusher_thread':
- instead of composed 'query_cache' object it now expects its component
'query_cache_core' and 'query_builder' as constructor arguments. This allows
to create an instance of the 'static_sql_context_builder' and 'query_cache'
directly inside the thread function.
- Instead of 'stopped_' boolean flag, we now have a state enumeration ('initial'
'initialization_failure', 'operational', 'stopped')
- the implementation no longer uses 'std::conditional_variable' for awaiting timer
events / termination requests. Instead, it just wakes up periodically (once per
second) and checks it it needs to reload the cache. This is necessary to be able
to respond to graceful termination requests like 'KILL CONNECTION' or shutdown
closing sessions at shutdown.
- added 'request_termination()' method used inside component 'deinit()' handler.
- 'do_periodic_reload()' function now looks more lake a state machine performing
different actions and making transitions to different states.
- added new logic to wait for Sessin Server availability inside the
'do_periodic_reload()' function.
Reworked 'thread_handler_context' class - it now uses 'mysql_command_thread'
service to initialize / deinitialize the thread for MySQL.
Various MTR test case that use dictionary functions updated with explicit
granting necessary privileges on the dictionary table to the
'mysql.session'@'localhost' internal MySQL user.
Added new 'component_masking_functions.flusher_thread_connection_reuse'
MTR test case that checks that the same MySQL internal connection (created
via 'mysql_command_xxx' services) can be used several times (without closing
and re-opening) by the background flusher thread.
Added new 'component_masking_functions.flusher_thread_immediate_restart'
MTR test case that check for proper behavior during server shutdown
immediately after installing the component.
Added new 'wait_for_component_uninstall.inc' MTR include file which can be used
to perform several attempts to 'UNINSTALL COMPONENT' until it succeeds or
reaches the max number of attempts.
PS-9148 feature: basic_sql_context_builder renamed to abstract_sql_context_builder
https://perconadev.atlassian.net/browse/PS-9148
Added a comment describing class hierarchy.
PS-9148 feature: Extended class description comments
https://perconadev.atlassian.net/browse/PS-9148
PS-9148 feature: query_cache[_core] renamed to term_cache[_core]
https://perconadev.atlassian.net/browse/PS-9148
Added class descriptions for 'term_cache_core' and 'term_cache'.
PS-9148 feature: query_builder transformed into a singleton
https://perconadev.atlassian.net/browse/PS-9148
Removed all the boilerplate code connected with passing 'query_builder'
trhough class hierarchy.
'query_builder_ptr' changed from 'std::shared_ptr' to 'std::unique_ptr'.
'term_cache_ptr' changed from 'std::shared_ptr' to 'std::unique_ptr'.
PS-9148 feature: Added missing debug_sync facility reset to the MTR test cases
https://perconadev.atlassian.net/browse/PS-9148
PS-9148 feature: Extended component_masking_functions.dictionary_operations
https://perconadev.atlassian.net/browse/PS-9148
'component_masking_functions.dictionary_operations' extended with more
checks for the case when 'mysql.session'@'localhost' system user does not
have enough privileges to access 'mysql.masking_dictionaries' table.
Also fixed checks for non-existing 'mysql.masking_dictionaries' table.
PS-9148 feature: Fixed expected American Express card number length
https://perconadev.atlassian.net/browse/PS-9148
PS-9148 feature: Added more comments about flusher thread termination
https://perconadev.atlassian.net/browse/PS-9148
PS-9148 feature: Reworked bookshelf class
'bookshelf' class reworked so that internally it now holds 'std::unordered_map'
of 'dictionary' objects instead of 'dictionary_ptr' objects.
PS-9148 feature: Fixed compilation problem with older STLs
https://perconadev.atlassian.net/browse/PS-9148
Although in recent versions of STL implementation is is OK to use
'std::unordered_map' with incomplete types, this is not true for STL coming
with GCC 11 (default on Ubuntu Jammy).
'bookshelf.hpp' header now includes 'dictionary.hpp"' instead of
'dictionary_fwd.hpp"' to resolve ths issue.
PS-9148 feature: Reworked flusher thread initialization
https://perconadev.atlassian.net/browse/PS-9148
Reworked Dictionary Flusher thread initialization logic.
We now establish internal server connection under the
'LOCK_server_shutting_down' lock and only if the Server is not shutting
down ('server_shutting_down' is still false). This helps to ensure that
component's 'deinit()' function (that is called with service registry locked) is not run concurrently with constructing an instance of the
'static_sql_context_builder' class (that attempts to acquire the same service registry lock).
'sql_context' class constructor now accepts 2 parameters:
* initialization_registry_locking_mode (used for establishing connections)
* operation_registry_locking_mode (used for closing connections)
instead of a single one 'registry_locking_mode'.
Because of these change component's 'deinit()' function can no longer fail.
As the result, there is no need in 'wait_for_component_uninstall.inc' anymore. Changed to simple 'UNINSTALL COMPONENT'.
PS-9148 feature: collation-aware lookups in Dictionary Cache
https://perconadev.atlassian.net/browse/PS-9148
Reworked Dictionary Cache data structures ('dictionary' and 'bookshelf'
classes): instead of storing 'utf8mb4' representations of terms in
'std::string's, we now use 'charset_string' instead. This allows to perform
case insensitive accesnt aware lookups in dictionary cache following
'utf8mb4_0900_ai_ci' collation rules.
Added new 'component_masking_functions.dictionary_ai_ci_lookups' MTR
test case that simulates various lookups of terms that differ only in case or
accent.
Co-Authored-By: Oleksandr Kachan <[email protected]>1 parent cdcd38a commit 4ef0bb3
File tree
74 files changed
+3857
-847
lines changed- components/masking_functions
- include/masking_functions
- src
- masking_functions
- mysql-test
- include
- suite/component_masking_functions
- r
- t
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
74 files changed
+3857
-847
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
31 | 35 | | |
| 36 | + | |
| 37 | + | |
32 | 38 | | |
33 | 39 | | |
| 40 | + | |
34 | 41 | | |
35 | 42 | | |
| 43 | + | |
| 44 | + | |
36 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
37 | 50 | | |
38 | 51 | | |
39 | 52 | | |
40 | 53 | | |
41 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
42 | 62 | | |
| 63 | + | |
43 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
44 | 69 | | |
45 | 70 | | |
| 71 | + | |
| 72 | + | |
46 | 73 | | |
47 | 74 | | |
| 75 | + | |
48 | 76 | | |
49 | 77 | | |
| 78 | + | |
50 | 79 | | |
51 | 80 | | |
52 | 81 | | |
| |||
Lines changed: 70 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
Lines changed: 30 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
Lines changed: 29 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
Lines changed: 7 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
25 | | - | |
26 | | - | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| 92 | + | |
| 93 | + | |
92 | 94 | | |
93 | 95 | | |
94 | 96 | | |
| |||
130 | 132 | | |
131 | 133 | | |
132 | 134 | | |
133 | | - | |
| 135 | + | |
134 | 136 | | |
| 137 | + | |
| 138 | + | |
135 | 139 | | |
136 | 140 | | |
137 | 141 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
Lines changed: 9 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
23 | | - | |
24 | | - | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
38 | 39 | | |
39 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
40 | 43 | | |
41 | 44 | | |
42 | 45 | | |
43 | 46 | | |
44 | 47 | | |
45 | 48 | | |
| 49 | + | |
46 | 50 | | |
47 | 51 | | |
| 52 | + | |
| 53 | + | |
48 | 54 | | |
49 | 55 | | |
50 | 56 | | |
| |||
Lines changed: 47 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
0 commit comments