Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fb-e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ jobs:
pmm_client_version: ${{ inputs.pmm_client_version || 'latest-tarball' }}
launchable_confidence: ${{ inputs.launchable_confidence || '100%' }}
pmm_qa_branch: ${{ github.event_name == 'pull_request' && github.head_ref || (inputs.pmm_qa_branch || 'main') }}
setup_services: '--database psmdb'
setup_services: '--database psmdb --database ps=8.4'
pmm_test_flag: '@rta'
workers: 1

51 changes: 51 additions & 0 deletions cli/tests/perconaMySqlServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,56 @@ test.describe('PMM Client CLI tests for Percona Server Database', { tag: '@perco
`Expected pmm-admin to honor --connection-timeout=5s, got ${output.durationMs.toFixed(0)} ms`,
).toBeGreaterThan(5_000);
});

test('Verify adding, changing and removing MySQL RTA Agent in pmm-admin CLI', async ({ }) => {
test.skip(adminVersion < 9, 'MySQL RTA is available from pmm-client version 3.9.0');
Comment thread
theTibi marked this conversation as resolved.

const serviceName = 'rta_mysql_service';

await test.step('add MySQL service for the RTA agent', async () => {
const output = await cli.exec(`docker exec ${containerName} pmm-admin add mysql --query-source=perfschema --username=${MYSQL_USER} --password=${MYSQL_PASSWORD} ${serviceName} ${ipPort}`);
await output.assertSuccess();
await output.outContains('MySQL Service added.');
});

const serviceId = (await cli.exec(`docker exec ${containerName} pmm-admin list | grep "${serviceName}" | awk -F" " '{print $4}'`)).getStdOutLines()[0];
const pmmAgentId = (await cli.exec(`docker exec ${containerName} pmm-admin list | grep pmm_agent | awk -F" " '{print $3}'`)).getStdOutLines()[0];

await test.step('add MySQL RTA agent and verify it reaches Running', async () => {
const output = await cli.exec(`docker exec ${containerName} pmm-admin inventory add agent rta-mysql-agent ${pmmAgentId} ${serviceId} ${MYSQL_USER} --password=${MYSQL_PASSWORD}`);
await output.assertSuccess();
await output.outContains('Real-Time Analytics MySQL agent added.');
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await expect(async () => {
const pmmAdminListOutput = await cli.exec(`docker exec ${containerName} pmm-admin list`);

await pmmAdminListOutput.outContains('rta_mysql_agent Running');
}).toPass({ intervals: [1_000], timeout: 60_000 });

await expect(async () => {
const pmmAdminListOutput = await cli.exec(`docker exec ${containerName} pmm-admin inventory list agents --service-id=${serviceId}`);

await pmmAdminListOutput.outContains('rta_mysql_agent Running');
}).toPass({ intervals: [1_000], timeout: 60_000 });
});

const rtaAgentId = (await cli.exec(`docker exec ${containerName} pmm-admin list | grep rta_mysql_agent | awk -F" " '{print $3}'`)).getStdOutLines()[0];

await test.step('change MySQL RTA agent log level and collect interval', async () => {
const output = await cli.exec(`docker exec ${containerName} pmm-admin inventory change agent rta-mysql-agent ${rtaAgentId} --log-level=debug --collect-interval=5s`);
await output.assertSuccess();
await output.outContains('Real-Time Analytics MySQL agent configuration updated.');
await output.outContains('changed log level to debug');
await output.outContains('changed collect interval to 5s');
});

await test.step('remove MySQL RTA agent and service', async () => {
const output = await cli.exec(`docker exec ${containerName} pmm-admin inventory remove agent ${rtaAgentId}`);
await output.assertSuccess();
await output.outContains('Agent removed.');

await removeMySQLService(containerName, serviceName);
});
});
});

16 changes: 16 additions & 0 deletions e2e_tests/fixtures/pmmTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import QueryAnalytics from '@pages/qan/queryAnalytics.page';
import RealTimeAnalyticsPage from '@pages/qan/rta/realTimeAnalytics.page';
import NodesPage from '@pages/inventory/nodes.page';
import MongoDBHelper from '@helpers/mongodb.helper';
import MySQLHelper from '@helpers/mysql.helper';
import VacuumDashboard from '@pages/dashboards/postgresql/vacuumDashboard';
import apiEndpoints from '@helpers/apiEndpoints';
import SettingsPage from '@pages/ha/settings.page';
Expand All @@ -31,6 +32,7 @@ const pmmTest = base.extend<{
dashboard: Dashboard;
grafanaHelper: GrafanaHelper;
mongoDbHelper: MongoDBHelper;
mySqlDbHelper: MySQLHelper;
api: Api;
qanStoredMetrics: QanStoredMetrics;
urlHelper: UrlHelper;
Expand Down Expand Up @@ -122,6 +124,20 @@ const pmmTest = base.extend<{

await use(mongoDbHelper);
},
mySqlDbHelper: async ({}, use) => {
// Defaults match the QA framework ps setup (host port 3317 -> sandbox 3307,
// dbdeployer msandbox credentials); override via env for local environments.
const mySqlDbHelper = new MySQLHelper({
database: process.env.MYSQL_DATABASE || 'test',
host: process.env.MYSQL_HOST || '127.0.0.1',
password: process.env.MYSQL_PASSWORD || 'msandbox',
port: Number(process.env.MYSQL_PORT) || 3_317,
username: process.env.MYSQL_USER || 'msandbox',
});

await use(mySqlDbHelper);
await mySqlDbHelper.close();
},
nodesPage: async ({ page }, use) => await use(new NodesPage(page)),
portalRemoval: async ({ page }, use) => {
const portalRemoval = new PortalRemoval(page);
Expand Down
1 change: 1 addition & 0 deletions e2e_tests/helpers/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const apiEndpoints = {
},
realtimeanalytics: {
queriesSearch: '/v1/realtimeanalytics/queries:search',
sessions: '/v1/realtimeanalytics/sessions',
sessionsStart: '/v1/realtimeanalytics/sessions:start',
sessionsStop: '/v1/realtimeanalytics/sessions:stop',
},
Expand Down
95 changes: 95 additions & 0 deletions e2e_tests/helpers/mysql.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import mysql, { Connection, Pool } from 'mysql2/promise';
import { Timeouts } from './timeouts';

interface MySqlConfig {
host?: string;
port: number;
username: string;
password: string;
database?: string;
}

export default class MySQLHelper {
private closed = false;
private config: mysql.PoolOptions;
// Connections running intentionally long queries; destroyed on close() so
// fixture teardown does not wait for SLEEP() statements to finish.
private longRunningConnections: Connection[] = [];
private pool: Pool;

constructor(config: MySqlConfig) {
this.config = {
connectTimeout: Timeouts.THIRTY_SECONDS,
database: config.database,
host: config.host || '127.0.0.1',
password: config.password,
port: config.port,
user: config.username,
};
this.pool = mysql.createPool(this.config);
}

close = async () => {
this.closed = true;

// destroy() drops the socket immediately instead of draining, so teardown
// is not blocked by long-running SLEEP() queries.
for (const connection of this.longRunningConnections) {
connection.destroy();
}

this.longRunningConnections = [];

await this.pool.end();
};

runQuery = async (sql: string) => await this.pool.query(sql);

/**
* Simulates a long-running query visible in sys.x$processlist (and therefore in RTA).
* SLEEP() keeps the statement in the processlist for the whole delay while consuming
* nothing; the label is selected as a string literal so the RTA query text can be
* filtered by it. mysql2's query() interpolates placeholders client-side (no server
* prepare), so the label stays visible in the processlist statement text.
*
* Runs on a dedicated connection that close() destroys, so tests do not have to
* wait for the full delay. Errors caused by that teardown are suppressed; anything
* else is rethrown so a query that could not run still fails the test.
*
* @param options.queryLabel - string selected by the query; use it to find this query in RTA
* @returns Resolves when the statement finishes or the helper is closed
*/
simulateLongRunningQuery = async (
options: {
delayMs?: number;
queryLabel?: string;
} = {},
) => {
const { delayMs = Timeouts.TEN_SECONDS, queryLabel = 'rta-simulated-query' } = options;
const delaySeconds = Math.max(1, Math.ceil(delayMs / 1_000));
let connection: Connection | undefined;

try {
connection = await mysql.createConnection(this.config);

if (this.closed) {
return null;
}

this.longRunningConnections.push(connection);

return await connection.query('SELECT ?, SLEEP(?)', [queryLabel, delaySeconds]);
} catch (error) {
if (this.closed) {
return null;
}

throw error;
} finally {
if (connection) {
this.longRunningConnections = this.longRunningConnections.filter((c) => c !== connection);
connection.destroy();
}
}
};
}
1 change: 1 addition & 0 deletions e2e_tests/node_modules
Loading
Loading