mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01: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
|
||||
|
||||
@column()
|
||||
declare installation_status: 'idle' | 'installing' | 'error'
|
||||
|
||||
@column()
|
||||
declare depends_on: string | null
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ZIM_STORAGE_PATH } from '../utils/fs.js'
|
|||
@inject()
|
||||
export class DockerService {
|
||||
private docker: Docker
|
||||
private activeInstallations: Set<string> = new Set()
|
||||
public static KIWIX_SERVICE_NAME = 'nomad_kiwix_serve'
|
||||
public static OLLAMA_SERVICE_NAME = 'nomad_ollama'
|
||||
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
|
||||
// This can happen if the service was created but not properly installed
|
||||
// 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)
|
||||
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 {
|
||||
success: true,
|
||||
|
|
@ -272,8 +300,12 @@ export class DockerService {
|
|||
`Finalizing installation of service ${service.service_name}...`
|
||||
)
|
||||
service.installed = true
|
||||
service.installation_status = 'idle'
|
||||
await service.save()
|
||||
|
||||
// Remove from active installs tracking
|
||||
this.activeInstallations.delete(service.service_name)
|
||||
|
||||
this._broadcast(
|
||||
service.service_name,
|
||||
'completed',
|
||||
|
|
@ -285,6 +317,8 @@ export class DockerService {
|
|||
'error',
|
||||
`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}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
transmit.broadcast('service-installation', {
|
||||
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',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
depends_on: null,
|
||||
},
|
||||
|
|
@ -41,6 +42,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
}),
|
||||
ui_location: null,
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: true,
|
||||
depends_on: null,
|
||||
},
|
||||
|
|
@ -61,6 +63,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
}),
|
||||
ui_location: '3000',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
depends_on: DockerService.OLLAMA_SERVICE_NAME,
|
||||
},
|
||||
|
|
@ -79,6 +82,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
}),
|
||||
ui_location: '8100',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
depends_on: null,
|
||||
},
|
||||
|
|
@ -99,6 +103,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
}),
|
||||
ui_location: '8200',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
depends_on: null,
|
||||
},
|
||||
|
|
@ -118,6 +123,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
}),
|
||||
ui_location: '8300',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
depends_on: null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
|||
|
||||
setIsInstalling(true)
|
||||
const response = await api.installService(serviceName)
|
||||
if (!response) {
|
||||
throw new Error('An internal error occurred while trying to install the service.')
|
||||
}
|
||||
if (!response.success) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
|
@ -102,6 +105,9 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
|
|||
try {
|
||||
setLoading(true)
|
||||
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) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user