Skip to content

Commit 8ab1b04

Browse files
Add SetupAdapter plugin type
1 parent 4509f48 commit 8ab1b04

File tree

9 files changed

+215
-19
lines changed

9 files changed

+215
-19
lines changed

src/main/kotlin/cloud/CloudManager.kt

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import agent.AgentRegistryFactory
1414
import cloud.template.AttachedVolume
1515
import cloud.template.ProvisioningTemplateExtension
1616
import com.fasterxml.jackson.module.kotlin.convertValue
17+
import db.PluginRegistry
18+
import db.PluginRegistryFactory
1719
import db.SetupRegistryFactory
1820
import db.VMRegistry
1921
import db.VMRegistryFactory
@@ -35,6 +37,7 @@ import kotlinx.coroutines.delay
3537
import kotlinx.coroutines.launch
3638
import model.cloud.PoolAgentParams
3739
import model.cloud.VM
40+
import model.plugins.call
3841
import model.retry.RetryPolicy
3942
import model.setup.Setup
4043
import model.setup.Volume
@@ -97,6 +100,22 @@ class CloudManager : CoroutineVerticle() {
97100
private const val CLUSTER_MAP_CIRCUIT_BREAKERS = "CloudManager.Map.CircuitBreakers"
98101
}
99102

103+
/**
104+
* Contains information about a VM to create
105+
*/
106+
private data class VMToCreate(
107+
/**
108+
* The VM to create (with the actual setup from which it should be created)
109+
*/
110+
val vm: VM,
111+
112+
/**
113+
* The original setup. May differ from the VM's setup if it has been
114+
* modified by setup adapter plugins.
115+
*/
116+
val originalSetup: Setup
117+
)
118+
100119
/**
101120
* The client to connect to the Cloud
102121
*/
@@ -156,6 +175,11 @@ class CloudManager : CoroutineVerticle() {
156175
*/
157176
private lateinit var setupCircuitBreakers: VMCircuitBreakerMap
158177

178+
/**
179+
* The plugin registry
180+
*/
181+
private val pluginRegistry: PluginRegistry = PluginRegistryFactory.create()
182+
159183
/**
160184
* A cluster-wide semaphore to prevent [sync] from being called multiple
161185
* times in parallel
@@ -475,7 +499,7 @@ class CloudManager : CoroutineVerticle() {
475499
if (!cleanupOnly) {
476500
// ensure there's a minimum number of VMs
477501
launch {
478-
createRemoteAgent { setupSelector.selectMinimum(setups) }
502+
createRemoteAgent(emptyList()) { setupSelector.selectMinimum(setups) }
479503
}
480504
}
481505
} finally {
@@ -561,21 +585,44 @@ class CloudManager : CoroutineVerticle() {
561585
break
562586
}
563587

564-
val result = createRemoteAgent { setupSelector.select(remaining, requiredCapabilities, possibleSetups) }
588+
val result = createRemoteAgent(requiredCapabilities) {
589+
setupSelector.select(remaining, requiredCapabilities, possibleSetups)
590+
}
565591
remaining = result.count { !it.second }.toLong()
566592
}
567593
}
568594

569-
private suspend fun createRemoteAgent(selector: suspend () -> List<Setup>): List<Pair<VM, Boolean>> {
595+
/**
596+
* Applies all setup adapter plugins to the given setup and returns the new
597+
* instance. If there are no plugins or if they did not make any modifications,
598+
* the method returns the original setup.
599+
*/
600+
private suspend fun applyPlugins(setup: Setup,
601+
requiredCapabilities: Collection<String>): Setup {
602+
val adapters = pluginRegistry.getSetupAdapters()
603+
var result = setup
604+
for (adapter in adapters) {
605+
result = adapter.call(result, requiredCapabilities, vertx)
606+
}
607+
return result
608+
}
609+
610+
private suspend fun createRemoteAgent(requiredCapabilities: Collection<String>,
611+
selector: suspend () -> List<Setup>): List<Pair<VM, Boolean>> {
570612
// atomically create VM entries in the registry
571613
val sharedData = vertx.sharedData()
572614
val lock = sharedData.getLock(LOCK_VMS).coAwait()
573615
val vmsToCreate = try {
574616
val setupsToCreate = selector()
575617
setupsToCreate.map { setup ->
576-
VM(setup = setup).also {
577-
vmRegistry.addVM(it)
578-
} to setup
618+
// call setup adapters and modify setup if necessary
619+
val modifiedSetup = applyPlugins(setup, requiredCapabilities)
620+
621+
// add VM to registry
622+
val vm = VM(setup = modifiedSetup)
623+
vmRegistry.addVM(vm)
624+
625+
VMToCreate(vm = vm, originalSetup = setup)
579626
}
580627
} finally {
581628
lock.release()
@@ -589,30 +636,30 @@ class CloudManager : CoroutineVerticle() {
589636
* Return a list that contains pairs of a VM and a boolean telling if the
590637
* VM was created successfully or not.
591638
*/
592-
private suspend fun createRemoteAgents(vmsToCreate: List<Pair<VM, Setup>>): List<Pair<VM, Boolean>> {
639+
private suspend fun createRemoteAgents(vmsToCreate: List<VMToCreate>): List<Pair<VM, Boolean>> {
593640
val sharedData = vertx.sharedData()
594-
val deferreds = vmsToCreate.map { (vm, setup) ->
641+
val deferreds = vmsToCreate.map { (vm, originalSetup) ->
595642
// create multiple VMs in parallel
596643
async {
597644
// hold a lock as long as we are creating this VM
598645
val creatingLock = sharedData.getLock(VM_CREATION_LOCK_PREFIX + vm.id).coAwait()
599646
try {
600-
log.info("Creating virtual machine ${vm.id} with setup `${setup.id}' ...")
647+
log.info("Creating virtual machine ${vm.id} with setup `${vm.setup.id}' ...")
601648

602-
val delay = setupCircuitBreakers.computeIfAbsent(setup).currentDelay
649+
val delay = setupCircuitBreakers.computeIfAbsent(originalSetup).currentDelay
603650
if (delay > 0) {
604651
log.info("Backing off for $delay milliseconds due to too many failed attempts.")
605652
delay(delay)
606653
}
607654

608655
try {
609656
// create VM
610-
val externalId = createVM(vm.id, setup)
657+
val externalId = createVM(vm.id, vm.setup)
611658
vmRegistry.setVMExternalID(vm.id, externalId)
612659
vmRegistry.setVMCreationTime(vm.id, Instant.now())
613660

614661
// create other volumes in background
615-
val volumeDeferreds = createVolumesAsync(externalId, setup)
662+
val volumeDeferreds = createVolumesAsync(externalId, vm.setup)
616663

617664
try {
618665
cloudClient.waitForVM(externalId, timeoutCreateVM)
@@ -628,7 +675,7 @@ class CloudManager : CoroutineVerticle() {
628675
vmRegistry.setVMStatus(vm.id, VM.Status.CREATING, VM.Status.PROVISIONING)
629676

630677
val attachedVolumes = volumes.map { AttachedVolume(it.first, it.second) }
631-
provisionVM(ipAddress, vm.id, externalId, setup, attachedVolumes)
678+
provisionVM(ipAddress, vm.id, externalId, vm.setup, attachedVolumes)
632679
} catch (e: Throwable) {
633680
vmRegistry.forceSetVMStatus(vm.id, VM.Status.DESTROYING)
634681
cloudClient.destroyVM(externalId, timeoutDestroyVM)
@@ -646,12 +693,16 @@ class CloudManager : CoroutineVerticle() {
646693

647694
vmRegistry.setVMStatus(vm.id, VM.Status.PROVISIONING, VM.Status.RUNNING)
648695
vmRegistry.setVMAgentJoinTime(vm.id, Instant.now())
649-
setupCircuitBreakers.afterAttemptPerformed(setup.id, true)
696+
// always call setupCircuitBreakers with original setup ID! (see
697+
// computeIfAbsent() call above)
698+
setupCircuitBreakers.afterAttemptPerformed(originalSetup.id, true)
650699
} catch (t: Throwable) {
651700
vmRegistry.forceSetVMStatus(vm.id, VM.Status.ERROR)
652701
vmRegistry.setVMReason(vm.id, t.message ?: "Unknown error")
653702
vmRegistry.setVMDestructionTime(vm.id, Instant.now())
654-
setupCircuitBreakers.afterAttemptPerformed(setup.id, false)
703+
// always call setupCircuitBreakers with original setup ID! (see
704+
// computeIfAbsent() call above)
705+
setupCircuitBreakers.afterAttemptPerformed(originalSetup.id, false)
655706
throw t
656707
}
657708
} finally {
@@ -661,7 +712,7 @@ class CloudManager : CoroutineVerticle() {
661712
}
662713

663714
return deferreds.mapIndexed { i, d ->
664-
vmsToCreate[i].first to try {
715+
vmsToCreate[i].vm to try {
665716
d.await()
666717
true
667718
} catch (t: Throwable) {
@@ -699,7 +750,7 @@ class CloudManager : CoroutineVerticle() {
699750
* objects that can be used to wait for the completion of the asynchronous
700751
* operation and to obtain the IDs of the created volumes.
701752
*/
702-
private suspend fun createVolumesAsync(externalId: String,
753+
private fun createVolumesAsync(externalId: String,
703754
setup: Setup): List<Deferred<Pair<String, Volume>>> {
704755
val metadata = mapOf(CREATED_BY to createdByTag, SETUP_ID to setup.id,
705756
VM_EXTERNAL_ID to externalId)

src/main/kotlin/db/PluginRegistry.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import model.plugins.ProcessChainAdapterPlugin
77
import model.plugins.ProcessChainConsistencyCheckerPlugin
88
import model.plugins.ProgressEstimatorPlugin
99
import model.plugins.RuntimePlugin
10+
import model.plugins.SetupAdapterPlugin
1011

1112
/**
1213
* Provides access to compiled plugins
@@ -28,6 +29,8 @@ class PluginRegistry(private val compiledPlugins: List<Plugin>) {
2829
.toMap()
2930
private val runtimes = compiledPlugins.filterIsInstance<RuntimePlugin>()
3031
.associateBy { it.supportedRuntime }
32+
private val setupAdapters = compiledPlugins.filterIsInstance<SetupAdapterPlugin>()
33+
.toResolved()
3134

3235
/**
3336
* Get a list of all plugins
@@ -63,4 +66,9 @@ class PluginRegistry(private val compiledPlugins: List<Plugin>) {
6366
* Get all process chain consistency checkers
6467
*/
6568
fun getProcessChainConsistencyCheckers() = processChainConsistencyCheckers
69+
70+
/**
71+
* Get all setup adapters
72+
*/
73+
fun getSetupAdapters() = setupAdapters
6674
}

src/main/kotlin/db/PluginRegistryFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import model.plugins.ProcessChainAdapterPlugin
1414
import model.plugins.ProcessChainConsistencyCheckerPlugin
1515
import model.plugins.ProgressEstimatorPlugin
1616
import model.plugins.RuntimePlugin
17+
import model.plugins.SetupAdapterPlugin
1718
import model.plugins.initializerPluginTemplate
1819
import model.plugins.outputAdapterPluginTemplate
1920
import model.plugins.processChainAdapterPluginTemplate
2021
import model.plugins.processChainConsistencyCheckerPluginTemplate
2122
import model.plugins.progressEstimatorPluginTemplate
2223
import model.plugins.runtimePluginTemplate
24+
import model.plugins.setupAdapterPluginTemplate
2325
import model.plugins.wrapPluginFunction
2426
import model.processchain.ProcessChain
27+
import model.setup.Setup
2528
import org.slf4j.LoggerFactory
2629
import java.io.File
2730
import java.net.URLClassLoader
@@ -243,6 +246,8 @@ object PluginRegistryFactory {
243246
f as KFunction<Double?>, ::progressEstimatorPluginTemplate.parameters))
244247
is RuntimePlugin -> plugin.copy(compiledFunction = wrapPluginFunction(
245248
f as KFunction<Unit>, ::runtimePluginTemplate.parameters))
249+
is SetupAdapterPlugin -> plugin.copy(compiledFunction = wrapPluginFunction(
250+
f as KFunction<Setup>, ::setupAdapterPluginTemplate.parameters))
246251
else -> throw RuntimeException("Unknown plugin type: ${plugin::class.java}")
247252
}
248253
}

src/main/kotlin/model/plugins/Plugin.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import kotlin.reflect.jvm.javaType
2323
JsonSubTypes.Type(value = ProcessChainAdapterPlugin::class, name = "processChainAdapter"),
2424
JsonSubTypes.Type(value = ProcessChainConsistencyCheckerPlugin::class, name = "processChainConsistencyChecker"),
2525
JsonSubTypes.Type(value = ProgressEstimatorPlugin::class, name = "progressEstimator"),
26-
JsonSubTypes.Type(value = RuntimePlugin::class, name = "runtime")
26+
JsonSubTypes.Type(value = RuntimePlugin::class, name = "runtime"),
27+
JsonSubTypes.Type(value = SetupAdapterPlugin::class, name = "setupAdapter")
2728
)
2829
interface Plugin {
2930
/**

src/main/kotlin/model/plugins/ProcessChainAdapterPlugin.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package model.plugins
22

33
import com.fasterxml.jackson.annotation.JsonIgnore
44
import io.vertx.core.Vertx
5-
import model.processchain.Argument
65
import model.processchain.ProcessChain
76
import model.workflow.Workflow
87
import kotlin.reflect.KFunction
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package model.plugins
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore
4+
import io.vertx.core.Vertx
5+
import model.setup.Setup
6+
import kotlin.reflect.KFunction
7+
import kotlin.reflect.full.callSuspend
8+
9+
/**
10+
* A setup adapter plugin is a function that can modify a [model.setup.Setup]
11+
* before one or more VMs are created from it. The function has the following
12+
* signature:
13+
*
14+
* suspend fun mySetupAdapter(setup: model.setup.Setup,
15+
* requiredCapabilities: Collection<String>,
16+
* vertx: io.vertx.core.Vertx): model.setup.Setup,
17+
*
18+
* The function will be called with a setup to modify and a collection of
19+
* required capabilities the modified setup should meet. The function should
20+
* return a new setup instance or the original one if no modifications were
21+
* necessary.
22+
*/
23+
data class SetupAdapterPlugin(
24+
override val name: String,
25+
override val scriptFile: String,
26+
override val version: String? = null,
27+
override val dependsOn: List<String> = emptyList(),
28+
29+
/**
30+
* The compiled plugin
31+
*/
32+
@JsonIgnore
33+
override val compiledFunction: KFunction<Setup> = throwPluginNeedsCompile()
34+
) : DependentPlugin
35+
36+
@Suppress("UNUSED_PARAMETER")
37+
internal fun setupAdapterPluginTemplate(setup: Setup,
38+
requiredCapabilities: Collection<String>, vertx: Vertx): Setup {
39+
throw NotImplementedError("This is just a template specifying the " +
40+
"function signature of a setup adapter plugin")
41+
}
42+
43+
suspend fun SetupAdapterPlugin.call(setup: Setup,
44+
requiredCapabilities: Collection<String>, vertx: Vertx): Setup {
45+
return if (this.compiledFunction.isSuspend) {
46+
this.compiledFunction.callSuspend(setup, requiredCapabilities, vertx)
47+
} else {
48+
this.compiledFunction.call(setup, requiredCapabilities, vertx)
49+
}
50+
}

0 commit comments

Comments
 (0)