mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-04 15:56:16 +02:00
fix(admin): improve duplicate install request handling
This commit is contained in:
parent
3e4985c3c7
commit
c46b75e63d
|
|
@ -33,6 +33,9 @@ export default class Service extends BaseModel {
|
||||||
})
|
})
|
||||||
declare installed: boolean
|
declare installed: boolean
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare installation_status: 'idle' | 'installing' | 'error'
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare depends_on: string | null
|
declare depends_on: string | null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { ZIM_STORAGE_PATH } from '../utils/fs.js'
|
||||||
@inject()
|
@inject()
|
||||||
export class DockerService {
|
export class DockerService {
|
||||||
private docker: Docker
|
private docker: Docker
|
||||||
|
private activeInstallations: Set<string> = new Set()
|
||||||
public static KIWIX_SERVICE_NAME = 'nomad_kiwix_serve'
|
public static KIWIX_SERVICE_NAME = 'nomad_kiwix_serve'
|
||||||
public static OLLAMA_SERVICE_NAME = 'nomad_ollama'
|
public static OLLAMA_SERVICE_NAME = 'nomad_ollama'
|
||||||
public static OPEN_WEBUI_SERVICE_NAME = 'nomad_open_webui'
|
public static OPEN_WEBUI_SERVICE_NAME = 'nomad_open_webui'
|
||||||
|
|
@ -153,6 +154,27 @@ export class DockerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if installation is already in progress (database-level)
|
||||||
|
if (service.installation_status === 'installing') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Service ${serviceName} installation is already in progress`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check with in-memory tracking (race condition protection)
|
||||||
|
if (this.activeInstallations.has(serviceName)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Service ${serviceName} installation is already in progress`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark installation as in progress
|
||||||
|
this.activeInstallations.add(serviceName)
|
||||||
|
service.installation_status = 'installing'
|
||||||
|
await service.save()
|
||||||
|
|
||||||
// Check if a service wasn't marked as installed but has an existing container
|
// Check if a service wasn't marked as installed but has an existing container
|
||||||
// This can happen if the service was created but not properly installed
|
// This can happen if the service was created but not properly installed
|
||||||
// or if the container was removed manually without updating the service status.
|
// or if the container was removed manually without updating the service status.
|
||||||
|
|
@ -167,7 +189,13 @@ export class DockerService {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const containerConfig = this._parseContainerConfig(service.container_config)
|
const containerConfig = this._parseContainerConfig(service.container_config)
|
||||||
await this._createContainer(service, containerConfig)
|
|
||||||
|
// Execute installation asynchronously and handle cleanup
|
||||||
|
this._createContainer(service, containerConfig)
|
||||||
|
.catch(async (error) => {
|
||||||
|
logger.error(`Installation failed for ${serviceName}: ${error.message}`)
|
||||||
|
await this._cleanupFailedInstallation(serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -272,8 +300,12 @@ export class DockerService {
|
||||||
`Finalizing installation of service ${service.service_name}...`
|
`Finalizing installation of service ${service.service_name}...`
|
||||||
)
|
)
|
||||||
service.installed = true
|
service.installed = true
|
||||||
|
service.installation_status = 'idle'
|
||||||
await service.save()
|
await service.save()
|
||||||
|
|
||||||
|
// Remove from active installs tracking
|
||||||
|
this.activeInstallations.delete(service.service_name)
|
||||||
|
|
||||||
this._broadcast(
|
this._broadcast(
|
||||||
service.service_name,
|
service.service_name,
|
||||||
'completed',
|
'completed',
|
||||||
|
|
@ -285,6 +317,8 @@ export class DockerService {
|
||||||
'error',
|
'error',
|
||||||
`Error installing service ${service.service_name}: ${error.message}`
|
`Error installing service ${service.service_name}: ${error.message}`
|
||||||
)
|
)
|
||||||
|
// Mark install as failed and cleanup
|
||||||
|
await this._cleanupFailedInstallation(service.service_name)
|
||||||
throw new Error(`Failed to install service ${service.service_name}: ${error.message}`)
|
throw new Error(`Failed to install service ${service.service_name}: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,6 +406,20 @@ export class DockerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _cleanupFailedInstallation(serviceName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const service = await Service.query().where('service_name', serviceName).first()
|
||||||
|
if (service) {
|
||||||
|
service.installation_status = 'error'
|
||||||
|
await service.save()
|
||||||
|
}
|
||||||
|
this.activeInstallations.delete(serviceName)
|
||||||
|
logger.info(`[DockerService] Cleaned up failed installation for ${serviceName}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[DockerService] Failed to cleanup installation for ${serviceName}: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _broadcast(service: string, status: string, message: string) {
|
private _broadcast(service: string, status: string, message: string) {
|
||||||
transmit.broadcast('service-installation', {
|
transmit.broadcast('service-installation', {
|
||||||
service_name: service,
|
service_name: service,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'services'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.string('installation_status').defaultTo('idle').notNullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('installation_status')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: '8090',
|
ui_location: '8090',
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: false,
|
is_dependency_service: false,
|
||||||
depends_on: null,
|
depends_on: null,
|
||||||
},
|
},
|
||||||
|
|
@ -41,6 +42,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: null,
|
ui_location: null,
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: true,
|
is_dependency_service: true,
|
||||||
depends_on: null,
|
depends_on: null,
|
||||||
},
|
},
|
||||||
|
|
@ -61,6 +63,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: '3000',
|
ui_location: '3000',
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: false,
|
is_dependency_service: false,
|
||||||
depends_on: DockerService.OLLAMA_SERVICE_NAME,
|
depends_on: DockerService.OLLAMA_SERVICE_NAME,
|
||||||
},
|
},
|
||||||
|
|
@ -79,6 +82,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: '8100',
|
ui_location: '8100',
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: false,
|
is_dependency_service: false,
|
||||||
depends_on: null,
|
depends_on: null,
|
||||||
},
|
},
|
||||||
|
|
@ -99,6 +103,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: '8200',
|
ui_location: '8200',
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: false,
|
is_dependency_service: false,
|
||||||
depends_on: null,
|
depends_on: null,
|
||||||
},
|
},
|
||||||
|
|
@ -118,6 +123,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
}),
|
}),
|
||||||
ui_location: '8300',
|
ui_location: '8300',
|
||||||
installed: false,
|
installed: false,
|
||||||
|
installation_status: 'idle',
|
||||||
is_dependency_service: false,
|
is_dependency_service: false,
|
||||||
depends_on: null,
|
depends_on: null,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
||||||
|
|
||||||
setIsInstalling(true)
|
setIsInstalling(true)
|
||||||
const response = await api.installService(serviceName)
|
const response = await api.installService(serviceName)
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('An internal error occurred while trying to install the service.')
|
||||||
|
}
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
throw new Error(response.message)
|
throw new Error(response.message)
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +105,9 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const response = await api.affectService(record.service_name, action)
|
const response = await api.affectService(record.service_name, action)
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('An internal error occurred while trying to affect the service.')
|
||||||
|
}
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
throw new Error(response.message)
|
throw new Error(response.message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user