In the context of the rapid development of cloud computing and microservice architecture, there is an increasing need to provide the ability to dynamically execute code for various programming languages with a guarantee of security, scalability and high performance. This article describes a project that implements code execution in an isolated environment, and discusses the advantages of the chosen architectural solution for a modern WEB IDE. The system is built on Go, uses gRPC for effective interservice interaction, Redis as a message broker and Docker to isolate the execution environment. A WebSocket server is used to display results in real time.
We will describe in detail how the main components of the system are structured, how they differ from alternative solutions and why the choice of these technologies allows achieving high performance and security.
The project is built on the principle of microservice architecture, which allows you to divide functionality into independent services. Each component is responsible for a highly specialized task, which ensures flexibility, scalability, and fault tolerance of the system.
Main components:
Comparison: Unlike REST API, gRPC provides more efficient and reliable communication between services, which is critical for highly concurrent systems.
This project uses a microservice approach, which has a number of significant advantages:
When building modern WEB IDEs for remote code execution, various architectural solutions are often compared. Let’s consider two approaches:
Features:
This approach provides fast and reliable inter-service communication, high isolation of code execution, and flexible scaling due to containerization. It is perfect for modern WEB IDEs, where responsiveness and security are important.
Features:
Monolithic solutions, often used in early versions of web IDEs, are based on HTTP REST and centralized code execution. Such systems face scaling issues, increased latency, and difficulties in ensuring security when executing someone else’s code.
Note: In the modern context of WEB IDE development, the HTTP REST and centralized execution approach is inferior to the advantages of a microservices architecture, since it does not provide the necessary flexibility and scalability.
The graph clearly shows that the microservices architecture (Approach A) provides lower latency, higher throughput, better security and scalability compared to the monolithic solution (Approach B).
One of the key elements of system security and stability is the use of Docker. In our solution, all services are deployed in separate containers, which ensures:
Below is a minified version of the main sections of code that demonstrates how the system:
The system uses a global registry, where each language has its own runner. This allows you to easily add support for new languages, it is enough to implement the runner interface and register it:
package languages
import (
"errors"
"sync"
)
var (
registry = make(map[string]Runner)
registryMu sync.RWMutex
)
type Runner interface {
Validate(projectDir string) error
Compile(ctx context.Context, projectDir string) (<-chan string, error)
Run(ctx context.Context, projectDir string) (<-chan string, error)
}
func Register(language string, runner Runner) {
registryMu.Lock()
defer registryMu.Unlock()
registry[language] = runner
}
func GetRunner(language string) (Runner, error) {
registryMu.RLock()
defer registryMu.RUnlock()
if runner, exists := registry[language]; exists {
return runner, nil
}
return nil, errors.New("unsupported language")
}
// Example of registering a new language
func init() {
languages.Register("python", NewGenericRunner("python"))
languages.Register("javascript", NewGenericRunner("javascript"))
}
and receives the corresponding runner to execute the code.
runner, err := languages.GetRunner(req.Language)
For each user code request, a separate Docker container is created. This is done inside the runner methods (for example, in Run). The main logic for running the container is in the RunInDockerStreaming function:
package compiler
import (
"bufio"
"fmt"
"io"
"log"
"os/exec"
"time"
)
func RunInDockerStreaming(image, dir, cmdStr string, logCh chan < -string) error {
timeout: = 50 * time.Second
cmd: = exec.Command("docker", "run",
"--memory=256m", "--cpus=0.5", "--network=none",
"-v", fmt.Sprintf("%s:/app", dir), "-w", "/app",
image, "sh", "-c", cmdStr)
cmd.Stdin = nil
stdoutPipe,
err: = cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("error getting stdout: %v", err)
}
stderrPipe,
err: = cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error getting stderr: %v", err)
}
if err: = cmd.Start();err != nil {
return fmt.Errorf("Error starting command: %v", err)
}
// Reading logs from the container
go func() {
reader: = bufio.NewReader(io.MultiReader(stdoutPipe, stderrPipe))
for {
line, isPrefix, err: = reader.ReadLine()
if err != nil {
if err != io.EOF {
logCh < -fmt.Sprintf("[Error reading logs: %v]", err)
}
break
}
msg: = string(line)
for isPrefix {
more, morePrefix, err: = reader.ReadLine()
if err != nil {
break
}
msg += string(more)
isPrefix = morePrefix
}
logCh < -msg
}
close(logCh)
}()
doneCh: = make(chan error, 1)
go func() {
doneCh < -cmd.Wait()
}()
select {
case err:
= < -doneCh:
return err
case <-time.After(timeout):
if cmd.Process != nil {
cmd.Process.Kill()
}
return fmt.Errorf("Execution timed out")
}
}
This function generates the docker run command, where:
Thus, when calling the Run method of the runner, the following happens:
Minimized fragment of the main logic of code execution (executor.ExecuteCode):
func ExecuteCode(ctx context.Context, req CodeRequest, logCh chan string) CodeResponse {
// Create a temporary directory and write files
projectDir, err: = util.CreateTempProjectDir()
if err != nil {
return CodeResponse {
"", fmt.Sprintf("Error: %v", err)
}
}
defer os.RemoveAll(projectDir)
for fileName, content: = range req.Files {
util.WriteFileRecursive(filepath.Join(projectDir, fileName), [] byte(content))
}
// Get a runner for the selected language
runner, err: = languages.GetRunner(req.Language)
if err != nil {
return CodeResponse {
"", err.Error()
}
}
if err: = runner.Validate(projectDir);
err != nil {
return CodeResponse {
"", fmt.Sprintf("Validation error: %v", err)
}
}
// Compile (if needed) and run code in Docker container
compileCh, _: = runner.Compile(ctx, projectDir)
for msg: = range compileCh {
logCh < -"[Compilation]: " + msg
}
runCh, _: = runner.Run(ctx, projectDir)
var output string
for msg: = range runCh {
logCh < -"[Run]: " + msg
output += msg + "\n"
}
return CodeResponse {
Output: output
}
}
In this minimal example:
These key fragments show how the system supports extensibility (easy addition of new languages) and provides isolation by creating a separate Docker container for each request. This approach improves the security, stability and scalability of the platform, which is especially important for modern WEB IDEs.
This article discusses a platform for remote code execution built on a microservice architecture using the gRPC + Redis + Docker stack. This approach allows you to:
A comparative analysis shows that the microservice architecture significantly outperforms traditional monolithic solutions in all key metrics. The advantages of this approach are confirmed by real data, which makes it an attractive solution for creating high-performance and fault-tolerant systems.
Author: Oleksii Bondar
Date: 2025–02–07