Skip to content

Commit 27436f2

Browse files
authored
Postrelease dxapi 0.13.15 and revert it (#145)
* Release dxApi 0.13.15 * Postrelease and revert of 0.13.15 changes
1 parent 2690f9e commit 27436f2

5 files changed

Lines changed: 40 additions & 156 deletions

File tree

api/RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
* Revert preferring v3 instance types over v2, until v3 are more widely available.
6+
7+
## 0.13.15 (2025-11-14)
8+
59
* Prefer v3 instance types over v2
610

711
## 0.13.14 (2025-10-03)

api/src/main/resources/application.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
dxApi {
2-
version = "0.13.15-SNAPSHOT"
2+
version = "0.13.16-SNAPSHOT"
33
}
44

55
#

api/src/main/scala/dx/api/DxInstanceType.scala

Lines changed: 33 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ price comparative price
2727
package dx.api
2828

2929
import dx.api
30-
import dx.util.{Enum, JsUtils, Logger}
30+
import dx.util.{Enum, JsUtils, LinkedList, LinkedListInterface, LinkedNil, Logger}
3131
import dx.util.Enum.enumFormat
3232
import spray.json.{RootJsonFormat, _}
3333

@@ -70,7 +70,7 @@ case class InstanceTypeRequest(dxInstanceType: Option[String] = None,
7070
os: Option[ExecutionEnvironment] = None,
7171
optional: Boolean = false) {
7272
override def toString: String = {
73-
s"""memory=(${minMemoryMB},${maxMemoryMB}) disk=(${minDiskGB},${maxDiskGB}) diskType=${diskType}
73+
s"""memory=(${minMemoryMB},${maxMemoryMB}) disk=(${minDiskGB},${maxDiskGB}) diskType=${diskType}
7474
|cores=(${minCpu},${maxCpu}) gpu=${gpu} os=${os} instancetype=${dxInstanceType}
7575
|optional=${optional}""".stripMargin.replaceAll("\n", " ")
7676
}
@@ -118,8 +118,6 @@ case class DxInstanceType(name: String,
118118
priceRank: Option[Int] = None)
119119
extends Ordered[DxInstanceType] {
120120

121-
@transient private lazy val version: Int = DxInstanceType.typeVersion(name)
122-
123121
/**
124122
* Returns true if this instance type satisfies the requirements of `query`,
125123
* which happens if 1) this instance type has the same name as specified in
@@ -220,11 +218,12 @@ case class DxInstanceType(name: String,
220218
}
221219

222220
/**
223-
* Compare instances based on version (v1 vs v2 vs v3 vs ...).
224-
* Higher version number is always better.
221+
* Compare instances based on version (v1 vs v2) -
222+
* v2 instances are always better than v1 instances.
225223
*/
226224
def compareByType(that: DxInstanceType): Int = {
227-
this.version.compareTo(that.version)
225+
def typeVersion(name: String): Int = if (name contains DxInstanceType.Version2Suffix) 2 else 1
226+
typeVersion(this.name).compareTo(typeVersion(that.name))
228227
}
229228

230229
override def compare(that: DxInstanceType): Int = {
@@ -238,7 +237,7 @@ case class DxInstanceType(name: String,
238237
if (resCmp != 0) {
239238
resCmp
240239
} else {
241-
// all else being equal, choose newer versions (higher version number is better)
240+
// all else being equal, choose v2 instances over v1
242241
-compareByType(that)
243242
}
244243
}
@@ -254,33 +253,12 @@ object DxInstanceType extends DefaultJsonProtocol {
254253
implicit val dxInstanceTypeFormat: RootJsonFormat[DxInstanceType] = jsonFormat8(
255254
DxInstanceType.apply
256255
)
257-
258-
// Use a regex to dynamically find any version suffix like '_vN'
259-
val VersionRegex = "_v(\\d+)_".r
256+
val Version2Suffix = "_v2"
257+
val NewestVersion = "v2"
260258
val CpuSuffixStart = "x"
261259
val InstanceNameSeparator = "_"
262260
private val MemoryNormFactor: Double = 1024.0
263261
private val DiskNormFactor: Double = 16.0
264-
265-
/**
266-
* Extracts the numeric version from the instance type name.
267-
* Examples:
268-
* "mem1_ssd1_x4" -> 1 (default)
269-
* "mem1_ssd1_v2_x4" -> 2
270-
* "mem1_ssd1_v3_x4" -> 3
271-
*/
272-
def typeVersion(name: String): Int = {
273-
// Iterate over matches to find the last (most relevant) version suffix
274-
VersionRegex.findFirstMatchIn(name) match {
275-
case Some(m) =>
276-
try {
277-
m.group(1).toInt
278-
} catch {
279-
case _: NumberFormatException => 1
280-
}
281-
case _ => 1
282-
}
283-
}
284262
}
285263

286264
case class InstanceTypeDB(instanceTypes: Map[String, DxInstanceType]) {
@@ -292,63 +270,30 @@ case class InstanceTypeDB(instanceTypes: Map[String, DxInstanceType]) {
292270
instanceTypes.toVector.sortWith(_ < _).headOption
293271
}
294272

295-
/**
296-
* Generates the name of the next highest version of the current instance name.
297-
* If the current version is vN, it returns the string for v(N+1).
298-
* E.g., "mem1_ssd1_x4" (v1) -> "mem1_ssd1_v2_x4"
299-
* E.g., "mem1_ssd1_v2_x4" -> "mem1_ssd1_v3_x4"
300-
*
301-
* @param instance The DxInstanceType to upgrade.
302-
* @return The potential name of the next version instance.
303-
*/
304-
private def getNextVersionName(instance: DxInstanceType): String = {
305-
val currentName = instance.name
306-
val currentVersion = DxInstanceType.typeVersion(currentName)
307-
val nextVersion = currentVersion + 1
308-
val nextVersionSuffix = s"_v${nextVersion}"
309-
310-
// Explicitly match the type for the regex result to access start/end
311-
DxInstanceType.VersionRegex.findFirstMatchIn(currentName) match {
312-
case Some(m: scala.util.matching.Regex.Match) =>
313-
// Replace the existing version suffix with the next version suffix
314-
currentName.substring(0, m.start) + nextVersionSuffix + currentName.substring(m.end)
315-
case None =>
316-
// Append the version suffix before the CPU part
317-
val parts = currentName.split(DxInstanceType.InstanceNameSeparator).toVector
318-
val (prefix, suffix) = parts.partition(!_.startsWith(DxInstanceType.CpuSuffixStart))
319-
320-
(prefix :+ nextVersionSuffix.stripPrefix("_") :+ suffix.head)
321-
.mkString(DxInstanceType.InstanceNameSeparator)
322-
}
323-
}
324-
325273
private def newerVersionAvailable(instance: DxInstanceType): Boolean = {
326-
val nextVersionName = getNextVersionName(instance)
327-
instanceTypes.contains(nextVersionName)
274+
if (instance.name contains DxInstanceType.Version2Suffix) true
275+
else {
276+
instanceTypes.contains(upgradeToLatestVersion(instance))
277+
}
328278
}
329279

330280
private def upgradeToLatestVersion(instance: DxInstanceType): String = {
281+
val instanceNameElements = instance.name.split(DxInstanceType.InstanceNameSeparator).toVector
282+
val linkedElements = LinkedList.create(instanceNameElements)
331283
@tailrec
332-
def findLatest(currentName: String): String = {
333-
instanceTypes.get(currentName) match {
334-
case None =>
335-
currentName
336-
case Some(tempInstance) =>
337-
val nextVersionName = getNextVersionName(tempInstance)
338-
if (nextVersionName != currentName && instanceTypes.contains(nextVersionName)) {
339-
findLatest(nextVersionName)
340-
} else {
341-
currentName
342-
}
284+
def insertVersion(linkedList: LinkedListInterface[String],
285+
accu: Vector[String] = Vector.empty): Vector[String] = {
286+
linkedList match {
287+
case LinkedNil => accu
288+
case l: LinkedList[String] if l.next == LinkedNil => l.value +: accu
289+
case l: LinkedList[String]
290+
if l.value.startsWith(DxInstanceType.CpuSuffixStart)
291+
&& l.next.value != DxInstanceType.NewestVersion =>
292+
insertVersion(l.next, DxInstanceType.NewestVersion +: l.value +: accu)
293+
case l: LinkedList[(String)] => insertVersion(l.next, l.value +: accu)
343294
}
344295
}
345-
346-
val nextVersionName = getNextVersionName(instance)
347-
if (instanceTypes.contains(nextVersionName)) {
348-
findLatest(nextVersionName)
349-
} else {
350-
instance.name
351-
}
296+
insertVersion(linkedElements).mkString("_")
352297
}
353298

354299
/**
@@ -367,21 +312,12 @@ case class InstanceTypeDB(instanceTypes: Map[String, DxInstanceType]) {
367312
val preferredInstances = eligibleInstances.filterNot { instance =>
368313
instance.gpu || instance.name.contains("fpga")
369314
}
315+
val (v2Instances, v1Instances) =
316+
preferredInstances.partition(_.name.contains(DxInstanceType.Version2Suffix))
370317

371-
val instancesToConsider =
372-
if (preferredInstances.nonEmpty) preferredInstances else eligibleInstances
373-
374-
// Sort by version first (descending), then by price/resources (ascending)
375-
instancesToConsider.toVector
376-
.sortWith { (a, b) =>
377-
val versionCmp = -a.compareByType(b)
378-
if (versionCmp != 0) {
379-
versionCmp < 0
380-
} else {
381-
a.compare(b) < 0
382-
}
383-
}
384-
.headOption
318+
selectMinimalInstanceType(v2Instances) // Try preferred v2 non-GPU/FPGA
319+
.orElse(selectMinimalInstanceType(v1Instances)) // Then try v1 non-GPU/FPGA
320+
.orElse(selectMinimalInstanceType(eligibleInstances)) // As a last resort, consider all instances (including GPU/FPGA)
385321
.getOrElse(
386322
throw new Exception(
387323
s"""no instance types meet the minimal requirements memory >= ${InstanceTypeDB.MinMemory}
@@ -430,7 +366,8 @@ case class InstanceTypeDB(instanceTypes: Map[String, DxInstanceType]) {
430366
val instance = instanceTypes.get(name)
431367
instance match {
432368
case None => return instance
433-
case Some(x) if newerVersionAvailable(x) =>
369+
case Some(x)
370+
if newerVersionAvailable(x) && !(x.name contains DxInstanceType.Version2Suffix) =>
434371
Logger.get.warning(
435372
s"""
436373
|WARNING: an older version of the instance ${x.name} is specified.

api/src/test/scala/dx/api/InstanceTypeDBTest.scala

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -153,27 +153,6 @@ class InstanceTypeDBTest extends AnyFlatSpec with Matchers {
153153
)
154154
}
155155

156-
it should "defaultInstanceType. prefer v3 instance over v2" in {
157-
val db = createInstanceTypeDB(
158-
createTestInstance("mem1_ssd1_x4", 3072, 80),
159-
createTestInstance("mem1_ssd1_v2_x4", 3072, 80),
160-
createTestInstance("mem1_ssd1_v3_x4", 3072, 80),
161-
createTestInstance("mem1_ssd1_gpu1_x4", 3072, 80)
162-
)
163-
164-
db.defaultInstanceType.name shouldBe "mem1_ssd1_v3_x4"
165-
}
166-
167-
it should "defaultInstanceType. prefer gpu v3 instance if non-gpu v3, v2, or v1 exists" in {
168-
// All preferred (non-GPU/FPGA) instances are filtered out. Only GPU/FPGA remain.
169-
val db = createInstanceTypeDB(
170-
createTestInstance("mem1_ssd1_gpu1_v3_x4", 3072, 80),
171-
createTestInstance("mem1_ssd1_gpu1_v2_x4", 3072, 80)
172-
)
173-
174-
db.defaultInstanceType.name shouldBe "mem1_ssd1_gpu1_v3_x4"
175-
}
176-
177156
it should "selectOptimal. work on large instances (JIRA-1258)" in {
178157
val db = createInstanceTypeDB(
179158
createTestInstance("mem3_ssd1_x32", 245751, 597),
@@ -231,48 +210,12 @@ class InstanceTypeDBTest extends AnyFlatSpec with Matchers {
231210
db.selectOptimal(InstanceTypeRequest(minCpu = Some(8), gpu = Some(false))) shouldBe None
232211
}
233212

234-
it should "selectOptimal. prefer v3 over v2 over v1 when resources are equal" taggedAs EdgeTest in {
235-
val db = createInstanceTypeDB(
236-
createTestInstance("mem1_ssd1_x4", 8000, 80),
237-
createTestInstance("mem1_ssd1_v2_x4", 8000, 80),
238-
createTestInstance("mem1_ssd1_v3_x4", 8000, 80)
239-
)
240-
241-
db.selectOptimal(InstanceTypeRequest(minCpu = Some(4))) should matchPattern {
242-
case Some(instanceType: DxInstanceType) if instanceType.name == "mem1_ssd1_v3_x4" =>
243-
}
244-
}
245-
246-
it should "selectOptimal. GPU request prefers v3 GPU over v2 GPU" taggedAs EdgeTest in {
247-
val db = createInstanceTypeDB(
248-
createTestInstance("mem3_ssd1_gpu_v2_x8", 30000, 100),
249-
createTestInstance("mem3_ssd1_gpu_v3_x8", 30000, 100),
250-
createTestInstance("mem1_ssd1_v3_x4", 8000, 80)
251-
)
252-
253-
db.selectOptimal(InstanceTypeRequest(minCpu = Some(8), gpu = Some(true))) should matchPattern {
254-
case Some(instanceType: DxInstanceType) if instanceType.name == "mem3_ssd1_gpu_v3_x8" =>
255-
}
256-
}
257-
258213
it should "selectByName. issue a warning if requested a v1 instance by ID but v2 is available" in {
259214
testDb.selectByName("mem1_ssd1_x16") should matchPattern {
260215
case Some(instanceType: DxInstanceType) if instanceType.name == "mem1_ssd1_x16" =>
261216
}
262217
}
263218

264-
it should "selectByName. issue a warning if requested a v1 instance by ID but v3 is available" in {
265-
val db = createInstanceTypeDB(
266-
createTestInstance("mem1_ssd1_x16", 8000, 100),
267-
createTestInstance("mem1_ssd1_v2_x16", 8000, 100),
268-
createTestInstance("mem1_ssd1_v3_x16", 8000, 100)
269-
)
270-
271-
db.selectByName("mem1_ssd1_x16") should matchPattern {
272-
case Some(instanceType: DxInstanceType) if instanceType.name == "mem1_ssd1_x16" =>
273-
}
274-
}
275-
276219
it should "AWS region. Query returns correct pricing models for org and user" taggedAs ApiTest in {
277220
def instanceTypeFilter(instanceType: DxInstanceType): Boolean = {
278221
instanceType.os.exists(_.release == "24.04")
@@ -281,7 +224,7 @@ class InstanceTypeDBTest extends AnyFlatSpec with Matchers {
281224
val db = InstanceTypeDB.create(userBilltoProject, instanceTypeFilter)
282225

283226
db.instanceTypes.size shouldBe 133
284-
db.defaultInstanceType.name shouldBe "mem1_ssd2_v3_x2"
227+
db.defaultInstanceType.name shouldBe "mem1_ssd1_v2_x2"
285228
}
286229

287230
it should "OCI region. Query returns correct pricing models for org and user" taggedAs ApiTest in {

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ val yaml = project
112112
lazy val dependencies =
113113
new {
114114
val dxCommonVersion = "0.11.5"
115-
val dxApiVersion = "0.13.14"
115+
val dxApiVersion = "0.13.15"
116116
val typesafeVersion = "1.4.1"
117117
val sprayVersion = "1.3.6"
118118
val snakeyamlVersion = "2.3"

0 commit comments

Comments
 (0)