diff --git a/admin/app/jobs/run_benchmark_job.ts b/admin/app/jobs/run_benchmark_job.ts index f975e30..0ae41e8 100644 --- a/admin/app/jobs/run_benchmark_job.ts +++ b/admin/app/jobs/run_benchmark_job.ts @@ -3,6 +3,7 @@ import { QueueService } from '#services/queue_service' import { BenchmarkService } from '#services/benchmark_service' import type { RunBenchmarkJobParams } from '../../types/benchmark.js' import logger from '@adonisjs/core/services/logger' +import { DockerService } from '#services/docker_service' export class RunBenchmarkJob { static get queue() { @@ -18,7 +19,8 @@ export class RunBenchmarkJob { logger.info(`[RunBenchmarkJob] Starting benchmark ${benchmark_id} of type ${benchmark_type}`) - const benchmarkService = new BenchmarkService() + const dockerService = new DockerService() + const benchmarkService = new BenchmarkService(dockerService) try { let result diff --git a/admin/app/services/benchmark_service.ts b/admin/app/services/benchmark_service.ts index 2d4106a..3741d4a 100644 --- a/admin/app/services/benchmark_service.ts +++ b/admin/app/services/benchmark_service.ts @@ -1,7 +1,6 @@ import { inject } from '@adonisjs/core' import logger from '@adonisjs/core/services/logger' import transmit from '@adonisjs/transmit/services/main' -import Docker from 'dockerode' import si from 'systeminformation' import axios from 'axios' import { DateTime } from 'luxon' @@ -24,6 +23,7 @@ import type { RepositoryStats, } from '../../types/benchmark.js' import { randomUUID } from 'node:crypto' +import { DockerService } from './docker_service.js' // Re-export default weights for use in service const SCORE_WEIGHTS = { @@ -44,10 +44,6 @@ const BENCHMARK_CHANNEL = 'benchmark-progress' const AI_BENCHMARK_MODEL = 'llama3.2:1b' const AI_BENCHMARK_PROMPT = 'Explain recursion in programming in exactly 100 words.' -// Ollama API URL - configurable for Docker environments where localhost doesn't reach the host -// In Docker, use host.docker.internal (Docker Desktop) or the host gateway IP (Linux) -const OLLAMA_API_URL = process.env.OLLAMA_API_URL || 'http://host.docker.internal:11434' - // Reference scores for normalization (calibrated to 0-100 scale) // These represent "expected" scores for a mid-range system (score ~50) const REFERENCE_SCORES = { @@ -61,18 +57,10 @@ const REFERENCE_SCORES = { @inject() export class BenchmarkService { - private docker: Docker private currentBenchmarkId: string | null = null private currentStatus: BenchmarkStatus = 'idle' - constructor() { - const isWindows = process.platform === 'win32' - if (isWindows) { - this.docker = new Docker({ socketPath: '//./pipe/docker_engine' }) - } else { - this.docker = new Docker({ socketPath: '/var/run/docker.sock' }) - } - } + constructor(private dockerService: DockerService) {} /** * Run a full benchmark suite @@ -366,18 +354,25 @@ export class BenchmarkService { * Run AI benchmark using Ollama */ private async _runAIBenchmark(): Promise { + try { + this._updateStatus('running_ai', 'Running AI benchmark...') + const ollamaAPIURL = await this.dockerService.getServiceURL(DockerService.OLLAMA_SERVICE_NAME) + if (!ollamaAPIURL) { + throw new Error('AI Assistant service location could not be determined. Ensure AI Assistant is installed and running.') + } + // Check if Ollama is available try { - await axios.get(`${OLLAMA_API_URL}/api/tags`, { timeout: 5000 }) + await axios.get(`${ollamaAPIURL}/api/tags`, { timeout: 5000 }) } catch (error) { const errorCode = error.code || error.response?.status || 'unknown' throw new Error(`Ollama is not running or not accessible (${errorCode}). Ensure AI Assistant is installed and running.`) } // Check if the benchmark model is available, pull if not - const modelsResponse = await axios.get(`${OLLAMA_API_URL}/api/tags`) + const modelsResponse = await axios.get(`${ollamaAPIURL}/api/tags`) const models = modelsResponse.data.models || [] const hasModel = models.some((m: any) => m.name === AI_BENCHMARK_MODEL || m.name.startsWith(AI_BENCHMARK_MODEL.split(':')[0])) @@ -387,7 +382,7 @@ export class BenchmarkService { try { // Model pull can take several minutes, use longer timeout - await axios.post(`${OLLAMA_API_URL}/api/pull`, { name: AI_BENCHMARK_MODEL }, { timeout: 600000 }) + await axios.post(`${ollamaAPIURL}/api/pull`, { name: AI_BENCHMARK_MODEL }, { timeout: 600000 }) logger.info(`[BenchmarkService] Model ${AI_BENCHMARK_MODEL} downloaded successfully`) } catch (pullError) { throw new Error(`Failed to download AI benchmark model (${AI_BENCHMARK_MODEL}): ${pullError.message}`) @@ -397,9 +392,8 @@ export class BenchmarkService { // Run inference benchmark const startTime = Date.now() - try { const response = await axios.post( - `${OLLAMA_API_URL}/api/generate`, + `${ollamaAPIURL}/api/generate`, { model: AI_BENCHMARK_MODEL, prompt: AI_BENCHMARK_PROMPT, @@ -519,11 +513,11 @@ export class BenchmarkService { */ private async _ensureSysbenchImage(): Promise { try { - await this.docker.getImage(SYSBENCH_IMAGE).inspect() + await this.dockerService.docker.getImage(SYSBENCH_IMAGE).inspect() } catch { this._updateStatus('starting', `Pulling sysbench image...`) - const pullStream = await this.docker.pull(SYSBENCH_IMAGE) - await new Promise((resolve) => this.docker.modem.followProgress(pullStream, resolve)) + const pullStream = await this.dockerService.docker.pull(SYSBENCH_IMAGE) + await new Promise((resolve) => this.dockerService.docker.modem.followProgress(pullStream, resolve)) } } @@ -641,7 +635,7 @@ export class BenchmarkService { private async _runSysbenchCommand(cmd: string[]): Promise { try { // Create container with TTY to avoid multiplexed output - const container = await this.docker.createContainer({ + const container = await this.dockerService.docker.createContainer({ Image: SYSBENCH_IMAGE, Cmd: cmd, name: `${SYSBENCH_CONTAINER_NAME}_${Date.now()}`, diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 267e43f..b8491a6 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -18,6 +18,7 @@ export class DockerService { public static FLATNOTES_SERVICE_NAME = 'nomad_flatnotes' public static KOLIBRI_SERVICE_NAME = 'nomad_kolibri' public static BENCHMARK_SERVICE_NAME = 'nomad_benchmark' + public static NOMAD_NETWORK = 'project-nomad_default' constructor() { // Support both Linux (production) and Windows (development with Docker Desktop) @@ -124,11 +125,58 @@ export class DockerService { status: container.State, })) } catch (error) { - console.error(`Error fetching services status: ${error.message}`) + logger.error(`Error fetching services status: ${error.message}`) return [] } } + /** + * Get the URL to access a service based on its configuration. + * Attempts to return a docker-internal URL using the service name and exposed port. + * @param serviceName - The name of the service to get the URL for. + * @returns - The URL as a string, or null if it cannot be determined. + */ + async getServiceURL(serviceName: string): Promise { + if (!serviceName || serviceName.trim() === '') { + return null + } + + const service = await Service.query() + .where('service_name', serviceName) + .andWhere('installed', true) + .first() + + if (!service) { + return null + } + + const hostname = process.env.NODE_ENV === 'production' ? serviceName : 'localhost' + + // First, check if ui_location is set and is a valid port number + if (service.ui_location && parseInt(service.ui_location, 10)) { + return `http://${hostname}:${service.ui_location}` + } + + // Next, try to extract a host port from container_config + const parsedConfig = this._parseContainerConfig(service.container_config) + if (parsedConfig?.HostConfig?.PortBindings) { + const portBindings = parsedConfig.HostConfig.PortBindings + const hostPorts = Object.values(portBindings) + if (!hostPorts || !Array.isArray(hostPorts) || hostPorts.length === 0) { + return null + } + + const hostPortsArray = hostPorts.flat() as { HostPort: string }[] + const hostPortsStrings = hostPortsArray.map((binding) => binding.HostPort) + if (hostPortsStrings.length > 0) { + return `http://${hostname}:${hostPortsStrings[0]}` + } + } + + // Otherwise, return null if we can't determine a URL + return null + } + async createContainerPreflight( serviceName: string ): Promise<{ success: boolean; message: string }> { @@ -411,6 +459,14 @@ export class DockerService { ...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }), ...(containerConfig?.Env && { Env: containerConfig.Env }), ...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}), + // Ensure container is attached to the Nomad docker network in production + ...(process.env.NODE_ENV === 'production' && { + NetworkingConfig: { + EndpointsConfig: { + [DockerService.NOMAD_NETWORK]: {}, + }, + }, + }) }) this._broadcast( diff --git a/admin/commands/benchmark/results.ts b/admin/commands/benchmark/results.ts index 1583d20..d103713 100644 --- a/admin/commands/benchmark/results.ts +++ b/admin/commands/benchmark/results.ts @@ -19,8 +19,10 @@ export default class BenchmarkResults extends BaseCommand { } async run() { + const { DockerService } = await import('#services/docker_service') const { BenchmarkService } = await import('#services/benchmark_service') - const benchmarkService = new BenchmarkService() + const dockerService = new DockerService() + const benchmarkService = new BenchmarkService(dockerService) try { let results diff --git a/admin/commands/benchmark/run.ts b/admin/commands/benchmark/run.ts index 18ddb4e..bea387a 100644 --- a/admin/commands/benchmark/run.ts +++ b/admin/commands/benchmark/run.ts @@ -19,8 +19,10 @@ export default class BenchmarkRun extends BaseCommand { } async run() { + const { DockerService } = await import('#services/docker_service') const { BenchmarkService } = await import('#services/benchmark_service') - const benchmarkService = new BenchmarkService() + const dockerService = new DockerService() + const benchmarkService = new BenchmarkService(dockerService) // Determine benchmark type let benchmarkType: 'full' | 'system' | 'ai' = 'full' diff --git a/admin/commands/benchmark/submit.ts b/admin/commands/benchmark/submit.ts index e5e8b02..5a5aa35 100644 --- a/admin/commands/benchmark/submit.ts +++ b/admin/commands/benchmark/submit.ts @@ -16,8 +16,10 @@ export default class BenchmarkSubmit extends BaseCommand { } async run() { + const { DockerService } = await import('#services/docker_service') const { BenchmarkService } = await import('#services/benchmark_service') - const benchmarkService = new BenchmarkService() + const dockerService = new DockerService() + const benchmarkService = new BenchmarkService(dockerService) try { // Get the result to submit diff --git a/install/management_compose.yaml b/install/management_compose.yaml index 89ad831..2889cfd 100644 --- a/install/management_compose.yaml +++ b/install/management_compose.yaml @@ -32,7 +32,6 @@ services: - DB_SSL=false - REDIS_HOST=redis - REDIS_PORT=6379 - - OLLAMA_API_URL=http://host.docker.internal:11434 depends_on: mysql: condition: service_healthy