It’s not every day that you have to develop software for a giant telescope. However, when you do, it expands your understanding — and appreciation — of things you might have before taken for granted. In short, it forces you to experiment. We found that out when developing an orchestration toolkit for the Thirty Meter Telescope, a telescope being developed by a consortium of different organizations due to be launched in 2030. Thoughtworks was working with the Indian Institute of Astronomy (based in Bengaluru) and the TMT Project Office (based in Pasadena, CA.).
The central “experiment” that one particular challenge led us to was combining Kotlin and Scala to develop a domain specific language. We’ll go into more detail about how we did this shortly, but before we get to that it’s worth outlining the nature of the challenge.
The challenge
The orchestration toolkit is an important part of the telescope used by scientists to coordinate the complex network of the telescope’s components in various ways according to the observation they intend to do. In simple terms it is an execution engine; the domain experts (ie. the scientists) must input the logic required by writing scripts.
The challenge was that while much of the telescope’s software stack was developed in Scala, the scientists that would be using the orchestration toolkit and writing the scripts were familiar with languages like C++. Using Scala would be a steep learning curve.
While most of them were aware of basic programming constructs and had experience with scripting languages like Python, the main challenges for them were handling complex concepts and techniques such as multithreading and concurrency. While the scientists lacked knowledge of them, they were nevertheless critical to ensuring the orchestration toolkit could execute incredibly complex sequences and configurations.
The solution was to try and develop a user-friendly embedded DSL with minimal learning curve, which took care of concurrency and multithreading.
Now you’ve got the background of the project, we’ll show you how we did it, beginning with our first (and ultimately mistaken) attempt with Scala.
The first attempt: building a DSL with Scala
Initially, we chose Scala to implement the embedded DSL and Sequencer as the language’s `Future` and `async/await` APIs were effective for asynchronous operations. All data modifications inside the script were performed inside the Future APIs; these were executed on a single threaded executor. This meant the script was executed in a concurrent manner, ensuring there was no possibility of accessing data in a parallel manner. This was particularly useful as it eliminated problems like race conditions and corruption of the state due to parallel access.
A simplistic script created with our Scala DSL is shown below. The Script handles a command named ‘observationPrep’ by scheduling a command to a ‘motor1’ component at a given time.
// TcsSync.scala
class TcsSync(csw: CswServices) extends Script(csw) {
private val timeKey = KeyType.TAITimeKey.make(name = "time")
handleSetupCommand("observationPrep") { command =>
val executeAt = command(timeKey).head
spawn { // --------------> [ 1 ]
csw.scheduler.scheduleOnce(executeAt) {
spawn { // --------------> [ 2 ]
val moveCommand =
Setup(tcs.prefix, CommandName("move30Degrees"), command.maybeObsId)
csw.submit("motor1", moveCommand).await // --------------> [ 3 ]
}
}
Done
}
}
}
Here, spawn (at 1,2) is an alias for the async function and the .await (at 3) is an extension method for the await function. Scala provides the async and await functions to simplify the asynchronous code.
The async functions mark a block of the asynchronous code. The block usually contains one or more await calls, which marks a point where computation will be suspended until the awaited Future is complete.
At this stage, we — as a group of programmers — were fairly happy with the DSL. However, we were unsure about how easy to use it would be for the people actually writing scripts for the orchestration toolkit. A number of stakeholders also expressed concerns about the DSL’s complexity. This was a clear sign that we needed a more user-friendly alternative.
The problem with the Scala DSL was that the scripts required many asynchronous operations and Scala’s Future APIs placed demands on script writers to enter async/await statements for every single one. Obviously, this added significant complexity to the script writer’s task. Forgetting to write await statements could result in undesired behavior.
The example below demonstrates an instance where each function call is asynchronous.
spawn {
csw.submit("motor1", moveCommand).await
csw.publish(currentState()).await
csw.submit("motor2",moveCommand).await
csw.publish(currentState()).await
}
Scala’s approach to asynchronous programming, which is default async and explicit synchronous behavior, complicates script writers’ work. Also, because scripts would have to be written as a Scala class, it meant the script writers would have to learn an additional construct, which could lead to corner cases. Thus, we began a search for a better option.
In the second part of this blog series, we’ll go on to discuss how we used Kotlin and why it helped us solve this tricky challenge.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.