This IDE web operates your code in the cloud – without melting your laptop
In the context of the rapid development of cloud computing and architecture for precise services, there is an increasing need to provide the ability to implement a dynamic symbol for various programming languages with safety guarantee, expansion and high performance. This article describes a project that implements the implementation of the code in an isolated environment, and discusses the advantages of the chosen architectural solution for the modern web. The system was built on Go, GRPC is used for effective interaction between services, Redis as a messenger and Docker medium to isolate the implementation environment. WebsoCket server is used to display the results in actual time.
We will describe in detail how the main components of the system are organized, and how they differ from alternative solutions and why the selection of these technologies allows for high performance and safety.
The project was built on the principle of small services structure, allowing you to divide jobs into independent services. Each component is responsible for a very specialized task, which guarantees flexibility and expansion and tolerance with the error of the system.
Main ingredients:
- GRPC It is used to communicate between service. It is ideal for transferring data between microscopic services because of:
- The bilateral protocol (temporary stores of the protocol): ensures the transmission of fast data and pressure.
- Strict writing: It helps to avoid errors in transferring and processing data.
- Decreased arrival time: It is very important to internal calls between services (for example, between GRPC and Redis waiting list).
- Websock Service: It provides a two -way connection with the customer to send implementation results in the actual time. Participate in a waiting list with the results and re -directing data to the customer, providing an immediate offer for assembly and implementation records.
- Worker: An independent service that pulls the tasks from a waiting list, creates a temporary work environment, verifies the authenticity and implementation of software instructions in the isolated Docker container, then the implementation results are published again to the waiting list.
- Redis: It is used as a medium to transfer the tasks from the GRPC server to the worker and results from the worker to the Websock Server. Redis’s advantages are high speed, support/Sub and easy expansion.
- The software and Docker Runner: Unit responsible for operating Docker orders with a flow, allowing the actual time monitoring of the assembly and implementation process.
- Linguistic contestants: The logic combined to verify health and collect and implement the code for different languages (C, C ++, C#, Python, JavaScript, Typescript). Each runner carries out one interface, which extends the expansion of jobs for new languages.
Goes to go:
- Performance and expansion: Go has a high implementation speed, which is especially important to deal with a large number of parallel requests.
- Concortive Support: Goroutines and channels allow to implement the simultaneous interaction between the components without multiple multi -patterns.
GRPC’s advantages:
- Effective data transmission: Thanks to the bilateral transport protocol (temporary stores of the protocol), GRPC provides low transition time and loading a low network.
- Strong writing: This reduces the number of errors associated with the incorrect interpretation of data between precise services.
- Support for two -way flow: This is especially useful for exchange of records and effective implementation results.
Comparison: Unlike API REST, GRPC provides more efficient and reliable communication between services, which is very important to very simultaneous systems.
Why Redis?
- High performance: Redis can handle a large number of operations per second, making it ideal for the task and lists of results.
- PUB/Sub/List Support: The simplicity of implementing waiting lists and subscription mechanisms makes it easy to organize unsafe interactions between services.
- Compared with other messages: unlike RabbitmQ or Kafka, Redis requires less configuration and provides adequate performance of systems in actual time.
Docker:
- Environmental insulation: Docker contains you to run a symbol in a completely isolated environment, which increases the integrity of implementation and reduces the risk of conflict with the main system.
- Management and Consistency: Docker’s use provides the same environment to collect and implement software instructions, regardless of the host system.
- Comparison: The operation of the code can directly pose a threat to safety and lead to dependency conflicts, while Docker allows you to solve these problems.
Websockket
- Actual Time: Continuous contact with the customer allows data (records, implementation results) immediately.
- User experience: With Websockket, IDE can display the results of the code dynamically.
This project uses a microscopic services approach, which contains a number of important advantages:
- Independent scaling: Each service (GRPC server, worker, websockket, Redis) can be degraded separately according to download. This allows the effective use of resources and rapid adaptation to growth in the number of requests.
- Tolerance with errors: dividing the system into independent units means that the failure of microscopic services does not lead to the failure of the entire system. This increases the total stability and simplifies the recovery of errors.
- Flexibility of Development and Publishing: Small services are developed and published independently, which simplifies the introduction of new features and updates. This also allows you to use the most suitable technologies for each specific service.
- Ease of integration: clearly specific interfaces (for example via GRPC) makes it easy to connect new services without significant changes in the current structure.
- Isolation and safety: Every Microservice service can operate in its container, which reduces the risks associated with the implementation of an insecure code and provides an additional layer of protection.
When building modern web identifiers to implement code for remoteness, various architectural solutions are often compared. Let’s look at the approach:
Approach
- Cumin: 40 milliliters seconds
- Productivity: 90 units
- Security: 85 units
- Expansion: 90 units
Signs:
This approach provides a quick and reliable communication between service, high isolation for the implementation of software instructions, and flexible expansion due to containers. It is ideal for modern web identifiers, where response and safety are important.
Approach B: Traditional Homoing Architecture (HTTP REST + Central Implementation)
- Cumin: 70 mm seconds
- Productivity: 65 units
- Security: 60 units
- Expansion: 70 units
Signs:
Homogeneous solutions, which are often used in the early versions of IDES on the web, rely on Rest HTTP and implement the central code. Such systems face the problems of scaling, increased cumin, and difficulties in ensuring security when the symbol of another person is implemented.
Note: In the modern context of the development of IDE on the Internet, the REST HTTP and the central implementation approach is lower than the advantages of microscopic services, because it does not provide the necessary flexibility and expansion.
Imagine comparative standards
The graph clearly explains that the structure of microscopic services (Al -Najj A) provides less time, high productivity, and better security and expansion compared to the Stone Age solution (Al -Nahj B).
One of the main elements of system security and stability is the use of Docker. In our solution, all services are published in separate containers, ensuring:
- Implementation environment: Each service (GRPC server, worker, websockket), and the message broker (Redis) in its container, reduces the risk of unsafe symbol that affects the main system. At the same time, the code that the user works on in the browser (for example, through the IDE web) is created and executed in a separate Docker container for each task. This approach guarantees that the unsafe or wrong code cannot affect the main infrastructure.
- The consistency of the environment: Docker ensures that the settings remain the same in development, testing and production environments, which greatly simplifies the correction of errors and ensures the ability to predict the implementation of code.
- Flexibility of expansion: Each component can be limited independently, allowing you to effectively adapt to changing loads. For example, with an increase in the number of requests, you can run additional factor containers, each of which will create separate containers to implement the user code.
Below is a mini version of the main sections of code that shows how the system:
- It determines the language that must be turned on using the global hostility record.
- Docker’s container begins to run the user icon using the Runindockerstreaming function.
1. Discovering the language by registering hostility
The system uses a global record, where each language has its hostility. This allows you to add support for new languages easily, and it is sufficient to implement and register the Runner interface:
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"))
}
The hostile hostility is received for the implementation of the symbol.
runner, err := languages.GetRunner(req.Language)
2. The launch of the Docker container to implement the code
For each user symbol request, a separate Docker container is created. This is done inside the hostility methods (for example, in operation). The main logic of the container operation is present 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 job creates a Docker run order, where:
- The image is a specific Docker image of specific language (defined by hostility).
- Dir is the evidence with the symbol created for this request.
- CMDSTR is the matter to collect or implement the code.
Thus, when calling the running method, the following happens:
- The function of the Runindockerstreaming contains a Docker container where the code is performed.
- The implementation records are flowing on Logch, which allows you to transfer information about the actual implementation process.
3. Integrated implementation process
Reducing the main logic part of the code implementation (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 lower example:
- The language is detected by calling to languages.
- The launch of the Docker container is performed inside the assembly/operating methods, which use Runindockerstreaming to implement code in isolation.
These main fragments show how the expansion system supports (easy -to -languages add) and provides isolation by creating a separate Docker container for each request. This approach improves safety, stability and the ability to expand the statute, which is especially important for modern web identifiers.
This article discusses a platform for implementing the distant symbol based on the infrastructure of precise services using GRPC + Redis + Docker. This approach allows you:
- Reducing cumin and ensuring high productivity due to effective communication between services.
- Security guarantee by isolating the implementation of code in separate Docker containers, where a separate container is created for each user request.
- System limited flexibility due to the independent scaling of microscopic services.
- Providing results in the actual time via Websockket, which is especially important for modern web identifiers.
The comparative analysis shows that the structure of microscopic services greatly outperforms traditional homogeneous solutions in all major standards. The advantages of this approach are confirmed by real data, making it an attractive solution to create high -performance systems and bear errors.
Author: Oleksii Bondar
Date: 2025-02-07