Also known as the build stage of the SDLC, coding focuses on the writing and programming of a system. The Zones in this category take a hands-on approach to equip developers with the knowledge about frameworks, tools, and languages that they can tailor to their own build needs.
A framework is a collection of code that is leveraged in the development process by providing ready-made components. Through the use of frameworks, architectural patterns and structures are created, which help speed up the development process. This Zone contains helpful resources for developers to learn about and further explore popular frameworks such as the Spring framework, Drupal, Angular, Eclipse, and more.
Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
JavaScript (JS) is an object-oriented programming language that allows engineers to produce and implement complex features within web browsers. JavaScript is popular because of its versatility and is preferred as the primary choice unless a specific function is needed. In this Zone, we provide resources that cover popular JS frameworks, server applications, supported data types, and other useful topics for a front-end engineer.
Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.
Development and programming tools are used to build frameworks, and they can be used for creating, debugging, and maintaining programs — and much more. The resources in this Zone cover topics such as compilers, database management systems, code editors, and other software tools and can help ensure engineers are writing clean code.
Database Systems
Every modern application and organization collects data. With that, there is a constant demand for database systems to expand, scale, and take on more responsibilities. Database architectures have become more complex, and as a result, there are more implementation choices. An effective database management system allows for quick access to database queries, and an organization can efficiently make informed decisions. So how does one effectively scale a database system and not sacrifice its quality?Our Database Systems Trend Report offers answers to this question by providing industry insights into database management selection and evaluation criteria. It also explores database management patterns for microservices, relational database migration strategies, time series compression algorithms and their applications, advice for the best data governing practices, and more. The goal of this report is to set up organizations for scaling success.
The ReactAndGo project is used to compare a single page application frontend based on React and a Rest backend based on Go to Angular frontends and Spring Boot/Java backends. The goal of the project is to send out notifications to car drivers if the gas price falls below their target price. The gas prices are imported from a provider via MQTT messaging and stored in the database. For development, two test messages are provided that are sent to an Apache Artemis server to be processed in the project. The Apache Artemis server can be run as a Docker image, and the commands to download and run the image can be found in the 'docker-artemis.sh' file. As a database, Postgresql is used, and it can be run as a Docker image too. The commands can be found in the 'docker-postgres.sh' file. Architecture The system architecture looks like this: The React frontend uses the Rest interface that the Gin framework provides to communicate with the backend. The Apache Artemis Messaging Server is used in development to receive and send back the gas price test messages that are handled with the Paho-MQTT library. In production, the provider sends the MQTT messages. The Gorm framework is used to store the data in Postgresql. A push notification display is used to show the notification from the frontend if the target prices are reached. The open-source projects using Go have more of a domain-driven architecture that splits the code for each domain into packages. For the ReactAndGo project, the domain-driven architecture is combined with a layered architecture to structure the code. The common BaseController is needed to manage the routes and security of the application. The architecture is split between the gas station domain, the push notification domain, and the application user domain. The Rest request and response handling is in its own layer that includes the Rest client for the gas station import. The service layer contains the logic, database access, and other helper functions. Domain-independent functions like Cron Jobs, Jwt token handling, and message handling are implemented in separate packages that are in a utility role. Notifications From React Frontend to Go/Gin/Gorm Backend The ReactAndGo project is used to show how to display notifications with periodic requests to the backend and how to process rest requests in the backend in controllers and repositories. React Frontend In the front end, a dedicated worker is started after login that manages the notifications. The initWebWorker(...) function of the LoginModal.tsx starts the worker and handles the tokens: TypeScript-JSX const initWebWorker = async (userResponse: UserResponse) => { let result = null; if (!globalWebWorkerRefState) { const worker = new Worker(new URL('../webpush/dedicated-worker.js', import.meta.url)); if (!!worker) { worker.addEventListener('message', (event: MessageEvent) => { //console.log(event.data); if (!!event?.data?.Token && event?.data.Token?.length > 10) { setGlobalJwtToken(event.data.Token); } }); worker.postMessage({ jwtToken: userResponse.Token, newNotificationUrl: `/usernotification/new/${userResponse.Uuid}` } as MsgData); setGlobalWebWorkerRefState(worker); result = worker; } } else { globalWebWorkerRefState.postMessage({ jwtToken: userResponse.Token, newNotificationUrl: `/usernotification/new/${userResponse.Uuid}` } as MsgData); result = globalWebWorkerRefState; } return result; }; The React frontend uses the Recoil library for state management and checks if the globalWebWorkerRefState exists. If not, the worker in dedicated-worker.js gets created and the event listener for the Jwt tokens is created. The Jwt token is stored in a Recoil state to be used in all rest requests. Then the postMessage(...) method of the worker is called to start the requests for the notifications. Then the worker is stored in the globalWebWorkerRefState and the worker is returned. The worker is developed in the dedicated-worker.ts file. The worker is needed as .js file. To have the help of Typescript, the worker is developed in Typescript and then turned into Javascript in the Typescript Playground. That saves a lot of time for me. The refreshToken(...) function of the worker refreshes the Jwt tokens: TypeScript-JSX interface UserResponse { Token?: string Message?: string } let jwtToken = ''; let tokenIntervalRef: ReturnType<typeof setInterval>; const refreshToken = (myToken: string) => { if (!!tokenIntervalRef) { clearInterval(tokenIntervalRef); } jwtToken = myToken; if (!!jwtToken && jwtToken.length > 10) { tokenIntervalRef = setInterval(() => { const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken}` }, }; fetch('/appuser/refreshtoken', requestOptions).then(response => response.json() as UserResponse) .then(result => { if ((!result.Message && !!result.Token && result.Token.length > 10)) { //console.log('Token refreshed.'); jwtToken = result.Token; /* eslint-disable-next-line no-restricted-globals */ self.postMessage(result); } else { jwtToken = ''; clearInterval(tokenIntervalRef); } }); }, 45000); } } The refreshToken(...) function first checks if another token interval has been started and stops it. Then the token is assigned and checked. If it passes the check a new interval is started to refresh the token every 45 seconds. The requestOptions are created with the token in the Authorization header field. Then the new token is retrieved with fetch(...) , and the response is checked, the token is set, and it is posted to the EventListener in the LoginModal.tsx. If the Jwt token has not been received, the interval is stopped, and the jwtToken is set to an empty string. The Eventlistener of the worker receives the token message and processes it as follows: TypeScript-JSX interface MsgData { jwtToken: string; newNotificationUrl: string; } let notificationIntervalRef: ReturnType<typeof setInterval>; /* eslint-disable-next-line no-restricted-globals */ self.addEventListener('message', (event: MessageEvent) => { const msgData = event.data as MsgData; refreshToken(msgData.jwtToken); if (!!notificationIntervalRef) { clearInterval(notificationIntervalRef); } notificationIntervalRef = setInterval(() => { if (!jwtToken) { clearInterval(notificationIntervalRef); } const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken}` }, }; /* eslint-disable-next-line no-restricted-globals */ self.fetch(msgData.newNotificationUrl, requestOptions).then(result => result.json()).then(resultJson => { if (!!resultJson && resultJson?.length > 0) { /* eslint-disable-next-line no-restricted-globals */ self.postMessage(resultJson); //Notification //console.log(Notification.permission); if (Notification.permission === 'granted') { if(resultJson?.length > 0 && resultJson[0]?.Message?.length > 1 && resultJson[0]?.Title?.length > 1) { for(let value of resultJson) { new Notification(value?.Title, {body: value?.Message}); } } } } }); }, 60000); }); The addEventListener(...) method handles the MessageEvent messages with the MsgData. The jwtToken of the MsgData is used to start the refreshToken(...) function. Then it is checked to see if a notification interval has been started, and if so, it is stopped. Then a new interval is created that checks for new target-matching gas prices every 60 seconds. The jwtToken is checked, and if the check fails, the interval is stopped. Then the requestOptions are created with the Jwt token in the Authorization header field. Then fetch(...) is used to retrieve the new matching gas price updates. Then the result JSON is checked and posted back to the EventListener in the LoginModal.tsx. With Notification.permission the user gets asked for permission to send notifications, and granted means he agreed. The data for the notification is checked, and the notification is sent with new Notification(...). Backend To handle the frontend requests, the Go backend uses the Gin framework. The Gin framework provides the needed functions to handle Rest requests, like a router, context (url related stuff), TLS support, and JSON handling. The route is defined in the basecontroller.go: Go func Start(embeddedFiles fs.FS) { router := gin.Default() ... router.GET("/usernotification/new/:useruuid", token.CheckToken, getNewUserNotifications) ... router.GET("/usernotification/current/:useruuid", token.CheckToken, getCurrentUserNotifications) router.StaticFS("/public", http.FS(embeddedFiles)) router.NoRoute(func(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, "/public") }) absolutePathKeyFile := strings.TrimSpace(os.Getenv("ABSOLUTE_PATH_KEY_FILE")) absolutePathCertFile := strings.TrimSpace(os.Getenv("ABSOLUTE_PATH_CERT_FILE")) myPort := strings.TrimSpace(os.Getenv("PORT")) if len(absolutePathCertFile) < 2 || len(absolutePathKeyFile) < 2 || len(myPort) < 2 { router.Run() // listen and serve on 0.0.0.0:3000 } else { log.Fatal(router.RunTLS(":"+myPort, absolutePathCertFile, absolutePathKeyFile)) } } The Start function gets the embedded files for the /public directory with the static frontend files. The line: Go router.GET("/usernotification/new/:useruuid", token.CheckToken, getNewUserNotifications) Creates the route /usernotification/new/:useruuid with the useruuid as parameter. The CheckToken function in the token.go file handles the Jwt Token validation. The getNewUserNotifications function in the in the uncontroller.go handles the requests. The getNewUserNotifications(...) function: Go func getNewUserNotifications(c *gin.Context) { userUuid := c.Param("useruuid") myNotifications := notification.LoadNotifications(userUuid, true) c.JSON(http.StatusOK, mapToUnResponses(myNotifications)) } ... func mapToUnResponses(myNotifications []unmodel.UserNotification) []unbody.UnResponse { var unResponses []unbody.UnResponse for _, myNotification := range myNotifications { unResponse := unbody.UnResponse{ Timestamp: myNotification.Timestamp, UserUuid: myNotification.UserUuid, Title: myNotification.Title, Message: myNotification.Message, DataJson: myNotification.DataJson, } unResponses = append(unResponses, unResponse) } return unResponses } The getNewUserNotifications(…) function uses the Gin context to get the path parameter useruuid and then calls the LoadNotifications(…) function of the repository with it. The result is turned into UserNotifications with the mapToUnResponses(…) function, which sends only the data needed by the frontend. The Gin context is used to return the HTTP status OK and to marshal the UserNotifications to JSON. The function LoadNotifications(...) is in the unrepo.go file and loads the notifications from the database with the Gorm framework: Go func LoadNotifications(userUuid string, newNotifications bool) []unmodel.UserNotification { var userNotifications []unmodel.UserNotification if newNotifications { database.DB.Transaction(func(tx *gorm.DB) error { tx.Where("user_uuid = ? and notification_send = ?", userUuid, !newNotifications) .Order("timestamp desc").Find(&userNotifications) for _, userNotification := range userNotifications { userNotification.NotificationSend = true tx.Save(&userNotification) } return nil }) } else { database.DB.Transaction(func(tx *gorm.DB) error { tx.Where("user_uuid = ?", userUuid).Order("timestamp desc").Find(&userNotifications) var myUserNotifications []unmodel.UserNotification for index, userNotification := range userNotifications { if index < 10 { myUserNotifications = append(myUserNotifications, userNotification) continue } tx.Delete(&userNotification) } userNotifications = myUserNotifications return nil }) } return userNotifications } The LoadNotifications(...) function checks if only new notifications are requested. Then a database transaction is created, and the new UserNotifications (notification.go) of the user file are selected, ordered newest first. The send flag is set to true to mark them as no longer new, and the UserNotifications are saved to the database. The transaction is then closed, and the notifications are returned. If the current notifications are requested, a database transaction is opened, and the UserNotifications of the user are selected, ordered newest first. The first 10 notifications of the user are appended to the myUserNotification slice, and the others are deleted from the database. Then the transaction is closed, and the notifications are returned. Conclusion This is the first React frontend for me, and I share my experience developing this frontend. React is a much smaller library than the Angular Framework and needs more extra libraries like Recoil for state management. Features like interval are included in the Angular RxJs library. React has much fewer features and needs more additional libraries to achieve the same result. Angular is better for use cases where the frontend needs more than basic features. React has the advantage of simple frontends. A React frontend that grows to medium size will need more design and architecture work to be comparable to an Angular solution and might take more effort during development due to its less opinionated design. My impression is: React is the kitplane that you have to assemble yourself. Angular is the plane that is rolled out of the factory. The Go/Gin/Gorm backend works well. The Go language is much simpler than Java and makes reading it fast. Go can be learned in a relatively short amount of time and has strict types and a multi-threading concept that Project Loom tries to add to Java. The Gin framework offers the features needed to develop the controllers and can be compared to the Spring Boot framework in features and ease of development. The Gorm framework offers the features needed to develop the repositories for database access and management and can be compared to the Spring Boot framework in terms of features and ease of development. The selling point of Go is its lower memory consumption and fast startup because it compiles to a binary and does not need a virtual machine. Go and Java have garbage collection. Java can catch up with Project Graal on startup time, but the medium- to large-sized examples have to be available and analyzed first for memory consumption. A decision can be based on developer skills, the amount of memory saved, and the expected future of Project Graal.
The console.log function — the poor man’s debugger — is every JavaScript developer’s best friend. We use it to verify that a certain piece of code was executed or to check the state of the application at a given point in time. We may also use console.warn to send warning messages or console.error to explain what happened when things have gone wrong. Logging makes it easy to debug your app during local development. But what about debugging your Node.js app while it’s running in a hosted cloud environment? The logs are kept on the server, to which you may or may not have access. How do you view your logs then? Most companies use application performance monitoring tools and observability tools for better visibility into their hosted apps. For example, you might send your logs to a log aggregator like Datadog, Sumo Logic, or Papertrail where logs can be viewed and queried. In this article, we’ll look at how we can configure an app that is hosted on Render to send its system logs to Papertrail by using Render Log Streams. By the end, you’ll have your app up and running — and logging — in no time. Creating Our Node.JS App and Hosting It With Render Render is a cloud hosting platform made for developers by developers. With Render, you can easily host your static sites, web services, cron jobs, and more. We’ll start with a simple Node.js and Express app for our demo. You can find the GitHub repo here. You can also view the app here. To follow along on your machine, fork the repo so that you have a copy running locally. You can install the project’s dependencies by running yarn install, and you can start the app by running yarn start. Easy enough! Render Log Streams demo app Now it’s time to get our app running on Render. If you don’t have a Render account yet, create one now. It’s free! Once you’re logged in, click the “New” button and then choose the “Web Service” option from the menu. Creating a new web service This will take you to the next page where you’ll select the GitHub repo you’d like to connect. If you haven’t connected your GitHub account yet, you can do so here. And if you have connected your GitHub account but haven’t given Render access to your specific repo yet, you can click the “Configure account” button. This will take you to GitHub, where you can grant access to all your repos or just a selection of them. Connecting your GitHub repo Back on Render, after connecting to your repo, you’ll be taken to a configuration page. Give your app a name (I chose the same name as my repo, but it can be anything), and then provide the correct build command (yarn, which is a shortcut for yarn install) and start command (yarn start). Choose your instance type (free tier), and then click the “Create Web Service” button at the bottom of the page to complete your configuration setup. Configuring your app With that, Render will deploy your app. You did it! You now have an app hosted on Render’s platform. Log output from your Render app’s first deployment Creating Our Papertrail Account Let’s now create a Papertrail account. Papertrail is a log aggregator tool that helps make log management easy. You can create an account for free — no credit card is required. Once you’ve created your account, click on the “Add your first system” button to get started. Adding your first system in Papertrail This will take you to the next page which provides you with your syslog endpoint at the top of the screen. There are also instructions for running an install script, but in our case, we don’t actually need to install anything! So just copy that syslog endpoint, and we’ll paste it in just a bit. Syslog endpoint Connecting Our Render App to Papertrail We now have an app hosted on Render, and we have a Papertrail account for logging. Let’s connect the two! Back in the Render dashboard, click on your avatar in the global navigation, then choose “Account Settings” from the drop-down menu. Render account settings Then in the secondary side navigation, click on the “Log Streams” tab. Once on that page, you can click the “Add Log Stream” button, which will open a modal. Paste your syslog endpoint from Papertrail into the “Log Endpoint” input field, and then click “Add Log Stream” to save your changes. Adding your log stream You should now see your Log Stream endpoint shown in Render’s dashboard. Render Log Stream dashboard Great! We’ve connected Render to Papertrail. What’s neat is that we’ve set up this connection for our entire Render account, so we don’t have to configure it for each individual app hosted on Render. Adding Logs to Our Render App Now that we have our logging configured, let’s take it for a test run. In our GitHub repo’s code, we have the following in our app.js file: JavaScript app.get('/', (req, res) => { console.log('Log - home page'); console.info('Info - home page'); console.warn('Warn - home page'); console.error('Error - home page'); console.debug('Debug - home page'); return res.sendFile('index.html', { root: 'public' }); }); When a request is made to the root URL of our app, we do a bit of logging and then send the index.html file to the client. The user doesn’t see any of the logs since these are server-side rather than client-side logs. Instead, the logs are kept on our server, which, again, is hosted on Render. To generate the logs, open your demo app in your browser. This will trigger a request for the home page. If you’re following along, your app URL will be different from mine, but my app is hosted here. Viewing Logs in Papertrail Let’s go find those logs in Papertrail. After all, they were logged to our server, but our server is hosted on Render. In your Papertrail dashboard, you should see at least two systems: one for Render itself, which was used to test the account connection, and one for your Render app (“render-log-stream-demo” in my case). Papertrail systems Click on the system for your Render app, and you’ll see a page where all the logs are shown and tailed, with the latest logs appearing at the bottom of the screen. Render app logs in Papertrail You can see that we have logs for many events, not just the data that we chose to log from our app.js file. These are the syslogs, so you also get helpful log data from when Render was installing dependencies and deploying your app! At the bottom of the page, we can enter search terms to query our logs. We don’t have many logs here yet, but where you’re running a web service that gets millions of requests per day, these log outputs can get very large very quickly. Searching logs in Papertrail Best Practices for Logging This leads us to some good questions: Now that we have logging set up, what exactly should we be logging? And how should we be formatting our logs so that they’re easy to query when we need to find them? What you’re logging and why you’re logging something will vary by situation. You may be adding logs after a customer issue is reported that you’re unable to reproduce locally. By adding logs to your app, you can get better visibility into what’s happening live in production. This is a reactive form of logging in which you’re adding new logs to certain files and functions after you realize you need them. As a more proactive form of logging, there may be important business transactions that you want to log all the time, such as account creation or order placement. This will give you greater peace of mind that events are being processed as expected throughout the day. It will also help you see the volume of events generated in any given interval. And, when things do go wrong, you’ll be able to pinpoint when your log output changed. How you format your logs is up to you, but you should be consistent in your log structure. In our example, we just logged text strings, but it would be even better to log our data in JSON format. With JSON, we can include key-value pairs for all of our messages. For each message, we might choose to include data for the user ID, the timestamp, the actual message text, and more. The beauty of JSON is that it makes querying your logs much easier, especially when viewing them in a log aggregator tool that contains thousands or millions of other messages. Conclusion There you have it — how to host your app on Render and configure logging with Render Log Streams and Papertrail. Both platforms only took minutes to set up, and now we can manage our logs with ease. Keep in mind that Render Log Streams let you send your logs to any of several different log aggregators, giving you lots of options. For example, Render logs can be sent to Sumo Logic. You just need to create a Cloud Syslog Source in your Sumo Logic account. Or, you can send your logs to Datadog as well. With that, it’s time for me to log off. Thanks for reading, Happy coding, and happy logging!
We want to save our thumbnail data to a database so that we can render our pictures to a nice HTML gallery page and finish the proof of concept for our Google Photos clone! Which database should we use and why? Which Java database API? What database tools will make our lives easier along the way? Find out in this episode of Marco Codes! What’s in the Video 00:00 Intro We'll cover what the plan is for this episode: to add database capabilities to our Google Photos clone, which currently only works with files, but doesn't store their metadata in a database table. 00:52 Where We Left Off Before jumping straight into implementing database and ORM features, we will do a quick code recap of the previous episodes, to remind ourselves how the image scanning and conversion process currently works. 01:46 Setup Whenever we want to do something with databases and Java, we need a couple of (in this case) Maven dependencies. More specifically we want to make sure to add the H2 database to our project, which we will use for production, not just for testing! We'll also add the HikariCP connection pool to it - something I do by default in every project and which is usually done automatically by frameworks like Spring Boot. 04:38 Writing a Database Schema Here, I present my current approach when doing Java database work: making sure the database schema is hand-written, thinking through table names, column names, types, etc. Hence, we'll start writing a schema.sql file for our new "media" table during this section. 10:08 Creating a DataSource Having created the schema, we'll need to create a DataSource next. As we're using HikariCP, we'll follow its documentation pages to set up the DataSource. We'll also make sure the schema.sql file written earlier gets automatically executed whenever we run our application. 12:46 Saving Thumbnail Data It's finally time to not just render thumbnail files on disk, but also save information about the generated thumbnails and original images in our brand-new database table! We'll use plain JDBC to do that and talk about its advantages and disadvantages. 14:00 Refactoring Maneuver Sometimes you just need to _see_ certain things that are very hard to explain in words. To clean up our program, we will have to change a couple of method signatures and move parameters up and down throughout the file. 16:21 Extracting Image Creation Dates At the moment, we don't properly detect the image creation date from its metadata. We'll talk about how to implement this in the future and why we'll stick with the file creation date for now. 17:10 Avoiding Duplication We'll also need to handle duplicates. If we re-run our program several times, we don't want to store the image metadata multiple times in our tables. Let's fix this here. 19:04 Inspecting H2 File DBs In case you don't know how to access H2 file databases, we will spend some time showing you how to do that from inside IntelliJ IDEA and its database tool window. 21:23 Rendering HTML Output Last but not least, we'll need to render all the information from our database to a nice, little HTML page, so we can actually browse our thumbnails! As a bonus point, this will be the simplest and probably dirtiest implementation of such an HTML page you've seen for a while - but it works! 30:30 What’s Next? Did you like what you saw? Which feature should we implement next? Let me know! Video
In this post, we'll delve into the fascinating world of operator overloading in Java. Although Java doesn't natively support operator overloading, we'll discover how Manifold can extend Java with that functionality. We'll explore its benefits, limitations, and use cases, particularly in scientific and mathematical code. We will also explore three powerful features provided by Manifold that enhance the default Java-type safety while enabling impressive programming techniques. We'll discuss unit expressions, type-safe reflection coding, and fixing methods like equals during compilation. Additionally, we'll touch upon a solution that Manifold offers to address some limitations of the var keyword. Let's dive in! Before we begin, as always, you can find the code examples for this post and other videos in this series on my GitHub page. Be sure to check out the project, give it a star, and follow me on GitHub to stay updated! Arithmetic Operators Operator overloading allows us to use familiar mathematical notation in code, making it more expressive and intuitive. While Java doesn't support operator overloading by default, Manifold provides a solution to this limitation. To demonstrate, let's start with a simple Vector class that performs vector arithmetic operations. In standard Java code, we define variables, accept them in the constructor, and implement methods like plus for vector addition. However, this approach can be verbose and less readable. Java public class Vec { private float x, y, z; public Vec(float x, float y, float z) { this.x = x; this.y = y; this.z = z; } public Vec plus(Vec other) { return new Vec(x + other.x, y + other.y, z + other.z); } } With Manifold, we can simplify the code significantly. Using Manifold's operator overloading features, we can directly add vectors together using the + operator as such: Java Vec vec1 = new Vec(1, 2, 3); Vec vec2 = new Vec(1, 1, 1); Vec vec3 = vec1 + vec2; Manifold seamlessly maps the operator to the appropriate method invocation, making the code cleaner and more concise. This fluid syntax resembles mathematical notation, enhancing code readability. Moreover, Manifold handles reverse notation gracefully. Suppose we reverse the order of the operands, such as a scalar plus a vector, Manifold swaps the order and performs the operation correctly. This flexibility enables us to write code in a more natural and intuitive manner. Let’s say we add this to the Vec class: Java public Vec plus(float other) { return new Vec(x + other, y + other, z + other); } This will make all these lines valid: Java vec3 += 5.0f; vec3 = 5.0f + vec3; vec3 = vec3 + 5.0f; vec3 += Float.valueOf(5.0f); In this code, we demonstrate that Manifold can swap the order to invoke Vec.plus(float) seamlessly. We also show that the plus equals operator support is built into the plus method support As implied by the previous code, Manifold also supports primitive wrapper objects, specifically in the context of autoboxing. In Java, primitive types have corresponding wrapper objects. Manifold handles the conversion between primitives and their wrapper objects seamlessly, thanks to autoboxing and unboxing. This enables us to work with objects and primitives interchangeably in our code. There are caveats to this, as we will find out. BigDecimal Support Manifold goes beyond simple arithmetic and supports more complex scenarios. For example, the manifold-science dependency includes built-in support for BigDecimal arithmetic. BigDecimal is a Java class used for precise calculations involving large numbers or financial computations? By using Manifold, we can perform arithmetic operations with BigDecimal objects using familiar operators, such as +, -, *, and /. Manifold's integration with BigDecimal simplifies code and ensures accurate calculations. The following code is legal once we add the right set of dependencies, which add method extensions to the BigDecimal class: Java var x = new BigDecimal(5L); var y = new BigDecimal(25L); var z = x + y; Under the hood, Manifold adds the applicable plus, minus, times, etc. methods to the class. It does so by leveraging class extensions which I discussed before. Limits of Boxing We can also extend existing classes to support operator overloading. Manifold allows us to extend classes and add methods that accept custom types or perform specific operations. For instance, we can extend the Integer class and add a plus method that accepts BigDecimal as an argument and returns a BigDecimal result. This extension enables us to perform arithmetic operations between different types seamlessly. The goal is to get this code to compile: Java var z = 5 + x + y; Unfortunately, this won’t compile with that change. The number five is a primitive, not an Integer, and the only way to get that code to work would be: Java var z = Integer.valueOf(5) + x + y; This isn’t what we want. However, there’s a simple solution. We can create an extension to BigDecimal itself and rely on the fact that the order can be swapped seamlessly. This means that this simple extension can support the 5 + x + y expression without a change: Java @Extension public class BigDecimalExt { public static BigDecimal plus(@This BigDecimal b, int i) { return b.plus(BigDecimal.valueOf(i)); } } List of Arithmetic Operators So far, we focused on the plus operator, but Manifold supports a wide range of operators. The following table lists the method name and the operators it supports: Operator Method + , += plus -, -= minus *, *= times /, /= div %, %= rem -a unaryMinus ++ inc -- dec Notice that the increment and decrement operators don’t have a distinction between the prefix and postfix positioning. Both a++ and ++a would lead to the inc method. Index Operator The support for the index operator took me completely off guard when I looked at it. This is a complete game-changer… The index operator is the square brackets we use to get an array value by index. To give you a sense of what I’m talking about, this is valid code in Manifold: Java var list = List.of("A", "B", "C"); var v = list[0]; In this case, v will be “A” and the code is the equivalent of invoking list.get(0). The index operators seamlessly map to get and set methods. We can do assignments as well using the following: Java var list = new ArrayList<>(List.of("A", "B", "C")); var v = list[0]; list[0] = "1"; Notice I had to wrap the List in an ArrayList since List.of() returns an unmodifiable List. But this isn’t the part I’m reeling about. That code is “nice.” This code is absolutely amazing: Java var map = new HashMap<>(Map.of("Key", "Value")); var key = map["Key"]; map["Key"] = "New Value"; Yes! You’re reading valid code in Manifold. An index operator is used to lookup in a map. Notice that a map has a put() method and not a set method. That’s an annoying inconsistency that Manifold fixed with an extension method. We can then use an object to look up within a map using the operator. Relational and Equality Operators We still have a lot to cover… Can we write code like this (referring to the Vec object from before): Java if(vec3 > vec2) { // … } This won’t compile by default. However, if we add the Comparable interface to the Vec class this will work as expected: Java public class Vec implements Comparable<Vec> { // … public double magnitude() { return Math.sqrt(x x + y y + z * z); } @Override public int compareTo(Vec o) { return Double.compare(magnitude(), o.magnitude()); } } These >=, >, <, <= comparison operators will work exactly as expected by invoking the compareTo method. But there’s a big problem. You will notice that the == and != operators are missing from this list. In Java, we often use these operators to perform pointer comparisons. This makes a lot of sense in terms of performance. We wouldn’t want to change something so inherent in Java. To avoid that, Manifold doesn’t override these operators by default. However, we can implement the ComparableUsing interface, which is a sub-interface of the Comparable interface. Once we do that the == and != will use the equals method by default. We can override that behavior by overriding the method equalityMode() which can return one of these values: CompareTo — will use the compareTo method for == and != Equals (the default) — will use the equals method Identity — will use pointer comparison as is the norm in Java That interface also lets us override the compareToUsing(T, Operator) method. This is similar to the compareTo method but lets us create operator-specific behavior, which might be important in some edge cases. Unit Expressions for Scientific Coding Notice that Unit expressions are experimental in Manifold. But they are one of the most interesting applications of operator overloading in this context. Unit expressions are a new type of operator that significantly simplifies and enhances scientific coding while enforcing strong typing. With unit expressions, we can define notations for mathematical expressions that incorporate unit types. This brings a new level of clarity and types of safety to scientific calculations. For example, consider a distance calculation where speed is defined as 100 miles per hour. By multiplying the speed (miles per hour) by the time (hours), we can obtain the distance as such: Java Length distance = 100 mph * 3 hr; Force force = 5kg * 9.807 m/s/s; if(force == 49.035 N) { // true } The unit expressions allow us to express numeric values (or variables) along with their associated units. The compiler checks the compatibility of units, preventing incompatible conversions and ensuring accurate calculations. This feature streamlines scientific code and enables powerful calculations with ease. Under the hood, a unit expression is just a conversion call. The expression 100 mph is converted to: Java VelocityUnit.postfixBind(Integer.valueOf(100)) This expression returns a Velocity object. The expression 3 hr is similarly bound to the postfix method and returns a Time object. At this point, the Manifold Velocity class has a times method, which, as you recall, is an operator, and it’s invoked on both results: Java public Length times( Time t ) { return new Length( toBaseNumber() * t.toBaseNumber(), LengthUnit.BASE, getDisplayUnit().getLengthUnit() ); } Notice that the class has multiple overloaded versions of the times method that accept different object types. A Velocity times Mass will produce Momentum. A Velocity times Force results in Power. Many units are supported as part of this package even in this early experimental stage, check them out here. You might notice a big omission here: Currency. I would love to have something like: Java var sum = 50 USD + 70 EUR; If you look at that code, the problem should be apparent. We need an exchange rate. This makes no sense without exchange rates and possibly conversion costs. The complexities of financial calculations don’t translate as nicely to the current state of the code. I suspect that this is the reason this is still experimental. I’m very curious to see how something like this can be solved elegantly. Pitfalls of Operator Overloading While Manifold provides powerful operator overloading capabilities, it's important to be mindful of potential challenges and performance considerations. Manifold's approach can lead to additional method calls and object allocations, which may impact performance, especially in performance-critical environments. It's crucial to consider optimization techniques, such as reducing unnecessary method calls and object allocations, to ensure efficient code execution. Let’s look at this code: Java var n = x + y + z; On the surface, it can seem efficient and short. It physically translates to this code: Java var n = x.plus(y).plus(z); This is still hard to spot but notice that in order to create the result, we invoke two methods and allocate at least two objects. A more efficient approach would be: Java var n = x.plus(y, z); This is an optimization we often do for high-performance matrix calculations. You need to be mindful of this and understand what the operator is doing under the hood if performance is important. I don’t want to imply that operators are inherently slower. In fact, they’re as fast as a method invocation, but sometimes the specific method invoked and the number of allocations are unintuitive. Type Safety Features The following aren’t related to operator overloading, but they were a part of the second video, so I feel they make sense as part of a wide-sweeping discussion on type safety. One of my favorite things about Manifold is its support of strict typing and compile time errors. To me, both represent the core spirit of Java. JailBreak: Type-Safe Reflection @JailBreak is a feature that grants access to the private state within a class. While it may sound bad, @JailBreak offers a better alternative to using traditional reflection to access private variables. By jailbreaking a class, we can access its private state seamlessly, with the compiler still performing type checks. In that sense, it’s the lesser of two evils. If you’re going to do something terrible (accessing private state), then at least have it checked by the compiler. In the following code, the value array is private to String, yet we can manipulate it thanks to the @JailBreak annotation. This code will print “Ex0osed…”: Java @Jailbreak String exposedString = "Exposed..."; exposedString.value[2] = '0'; System.out.println(exposedString); JailBreak can be applied to static fields and methods as well. However, accessing static members requires assigning null to the variable, which may seem counterintuitive. Nonetheless, this feature provides a more controlled and type-safe approach to accessing the internal state, minimizing the risks associated with using reflection. Java @Jailbreak String str = null; str.isASCII(new byte[] { 111, (byte)222 }); Finally, all objects in Manifold are injected with a jailbreak() method. This method can be used like this (notice that fastTime is a private field): Java Date d = new Date(); long t = d.jailbreak().fastTime; Self Annotation: Enforcing Method Parameter Type In Java, certain APIs accept objects as parameters, even when a more specific type could be used. This can lead to potential issues and errors at runtime. However, Manifold introduces the @Self annotation, which helps enforce the type of object passed as a parameter. By annotating the parameter with @Self, we explicitly state that only the specified object type is accepted. This ensures type safety and prevents the accidental use of incompatible types. With this annotation, the compiler catches such errors during development, reducing the likelihood of encountering issues in production. Let’s look at the MySizeClass from my previous posts: Java public class MySizeClass { int size = 5; public int size() { return size; } public void setSize(int size) { this.size = size; } public boolean equals(@Self Object o) { return o != null && ((MySizeClass)o).size == size; } } Notice I added an equals method and annotated the argument with Self. If I remove the Self annotation, this code will compile: Java var size = new MySizeClass(); size.equals(""); size.equals(new MySizeClass()); With the @Self annotation, the string comparison will fail during compilation. Auto Keyword: A Stronger Alternative to Var I’m not a huge fan of the var keyword. I feel it didn’t simplify much, and the price is coding to an implementation instead of to an interface. I understand why the devs at Oracle chose this path. Conservative decisions are the main reason I find Java so appealing. Manifold has the benefit of working outside of those constraints, and it offers a more powerful alternative called auto. auto can be used in fields and method return values, making it more flexible than var. It provides a concise and expressive way to define variables without sacrificing type safety. Auto is particularly useful when working with tuples, a feature not yet discussed in this post. It allows for elegant and concise code, enhancing readability and maintainability. You can effectively use auto as a drop-in replacement for var. Finally Operator overloading with Manifold brings expressive and intuitive mathematical notation to Java, enhancing code readability and simplicity. While Java doesn't natively support operator overloading, Manifold empowers developers to achieve similar functionality and use familiar operators in their code. By leveraging Manifold, we can write more fluid and expressive code, particularly in scientific, mathematical, and financial applications. The type of safety enhancements in Manifold makes Java more… Well, “Java-like.” It lets Java developers build upon the strong foundation of the language and embrace a more expressive type-safe programming paradigm. Should we add operator overloading to Java itself? I'm not in favor. I love that Java is slow, steady, and conservative. I also love that Manifold is bold and adventurous. That way, I can pick it when I'm doing a project where this approach makes sense (e.g., a startup project) but pick standard conservative Java for an enterprise project.
I was recently involved in the TypeScript migration of the ZK Framework. For those who are new to ZK, ZK is the Java counterpart of the Node.js stack; i.e., ZK is a Java full-stack web framework where you can implement event callbacks in Java and control frontend UI with Java alone. Over more than a decade of development and expansion, we have reached a code base of more than 50K JavaScript and over 400K Java code, but we noticed that we are spending almost the same amount of time and effort in maintaining Java and JavaScript code, which means, in our project, JavaScript is 8 times harder to maintain than Java. I would like to share the reason we made the move to migrate from JavaScript to TypeScript, the options we evaluated, how we automated a large part of the migration, and how it changed the way we work and gave us confidence. The Problem ZK has been a server-centric solution for more than a decade. In recent years, we noticed the need for cloud-native support and have made this the main goal of our upcoming new version, ZK 10. The new feature will alleviate servers’ burden by transferring much of the model-view-model bindings to the client side so that the server side becomes as stateless as possible. This brings benefits such as reduced server memory consumption, simplified load balancing for ZK 10 clustered backends, and potentially easier integration with other frontend frameworks. We call this effort “Client MVVM.” However, this implies huge growth of JavaScript code. As we are already aware that JavaScript is harder to maintain, it is high time that we made our JavaScript codebase easier to work with at 50k lines of code. Otherwise, extending the existing JavaScript code with the whole MVVM stack will become Sisyphean, if not impossible. We started to look at why Java has higher productivity and how we can bring the same productivity to our client side. Why Does Java Beat JavaScript at Large-Scale Development? What did Java get right to enable us an 8x boost in productivity? We conclude that the availability of static analysis is the primary factor. We design and write programs long before programs are executed and often before compilation. Normally, we refactor, implement new features, and fix bugs by modifying source code instead of modifying the compiler-generated machine code or the memory of the live program. That is, programmers analyze programs statically (before execution) as opposed to dynamically (during execution). Not only is static analysis more natural to humans, but static analysis is also easier to automate. Nowadays, compilers not only generate machine code from source code but also perform the sort of analysis that humans would do on source code like name resolution, initialization guards, dead-code analysis, etc. Humans can still perform static analysis on JavaScript code. However, without the help of automated static analyzers (compilers and linters), reasoning with JavaScript code becomes extremely error-prone and time-consuming. What value does the following JavaScript function return? It’s actually undefined instead of 1. Surprised? JavaScript function f() { return 1 } Compare this with Java, where we have the compiler to aid our reasoning “as we type." With TypeScript, the compiler will perform “automatic semicolon insertion” analysis followed by dead code analysis, yielding: Humans can never beat the meticulousness of machines. By delegating this sort of monotonous but critical tasks to machines, we can free up a huge amount of time while achieving unprecedented reliability. How Can We Enable Static Analysis for JavaScript? We evaluated the following 6 options and settled on TypeScript due to its extensive ECMA standard conformance, complete support for all mainstream JS module systems, and massive ecosystem. We provide a comparison of them at the end of the article. Here is a short synopsis. Google’s Closure Compiler: All types are specified in JSDoc, thereby bloating code and making inline type assertion very clumsy Facebook’s Flow: A much smaller ecosystem in terms of tooling and libraries compared to TypeScript Microsoft’s TypeScript: The most mature and complete solution Scala.js: Subpar; emitted JavaScript code ReScript: Requires a paradigm shift to purely functional programming; otherwise, very promising Semi-Automated Migration to TypeScript Prior to the TypeScript migration, our JavaScript code largely consisted of prototype inheritance via our ad-hoc zk.$extends function, as shown on the left-hand side. We intend to transform it to the semantically equivalent TypeScript snippet on the right-hand side. JavaScript Module.Class = zk.$extends(Super, { field: 1, field_: 2, _field: 3, $define: { field2: function () { // Do something in setter. }, }, $init: function() {}, method: function() {}, method_: function() {}, _method: function() {}, }, { staticField: 1, staticField_: 2, _staticField: 3, staticMethod: function() {}, staticMethod_: function() {}, _staticMethod: function() {}, }); TypeScript export namespace Module { @decorator('meta-data') export class Class extends Super { public field = 1; protected field_ = 2; private _field = 3; private _field2?: T; public getField2(): T | undefined { return this._field2; } public setField2(field2: T): this { const old = this._field2; this._field2 = field2; if (old !== field2) { // Do something in setter. } return this; } public constructor() { super(); } public method() {} protected method_() {} private _method() {} public static staticField = 1; protected static staticField_ = 2; private static _staticField = 3; public static staticMethod() {} protected static staticMethod_() {} private static _staticMethod() {} } } There are hundreds of such cases among which many have close to 50 properties. If we were to rewrite manually, it would not only take a very long time but be riddled with typos. Upon closer inspection, the transformation rules are quite straightforward. It should be subject to automation! Then, the process would be fast and reliable. Indeed, it is a matter of parsing the original JavaScript code into an abstract syntax tree (AST), modifying the AST according to some specific rules, and consolidating the modified AST into formatted source code. Fortunately, there is jscodeshift that does the parsing and consolidation of source code and provides a set of useful APIs for AST modification. Furthermore, there is AST Explorer that acts as a real-time IDE for jscodeshift so we can develop our jscodeshift transformation script productively. Better yet, we can author a custom typescript-eslint rule that spawns the jscodeshift script upon the presence of zk.$extends. Then, we can automatically apply the transformation to the whole codebase with the command eslint --fix. Let’s turn to the type T in the example above. Since jscodeshift presents us with the lossless AST (including comments), we can author a visitor that extracts the @return JSDoc of getter() if it can be found; if not, we can let the visitor walk into the method body of getter() and try to deduce the type T, e.g., deduce T to be string if the return value of getter() is the concatenation of this._field2 with some string. If still no avail, specify T as void, so that after jscodeshift is applied, the TypeScript compiler will warn us about a type mismatch. This way we can perform as much automated inference as possible before manual intervention and the sections required for manual inspection will be accurately surfaced by the compiler due to our fault injection. Besides whole file transformations like jscodeshift that can only run in batch mode, the typescript-eslint project allows us to author small and precise rules that update source code in an IDE, like VSCode, in real-time. For instance, we can author a rule that marks properties of classes or namespaces that begin or end with single underscores as @internal, so that documentation extraction tools and type definition bundlers can ignore them: TypeScript export namespace N { export function _helper() {} export class A { /** * Description ... */ protected doSomething_() {} } } TypeScript export namespace N { /** @internal */ export function _helper() {} export class A { /** * Description ... * @internal */ protected doSomething_() {} } } Regarding the example above, one would have to determine the existence of property-associating JSDoc, the pre-existence of the @internal tag, and the position to insert the @internal tag if missing. Since typescript-eslint also presents us with a lossless AST, it is easy to find the associating JSDoc of class or namespace properties. The only non-trivial task left is to parse, transform, and consolidate JSDoc fragments. Fortunately, this can be achieved with the TSDoc parser. Similar to activating jscodeshift via typescript-eslint in the first example, this second example is a case of delegating JSDoc transformation to the TSDoc parser upon a typescript-eslint rule match. With sufficient knowledge of JavaScript, TypeScript, and their build systems, one can utilize jscodeshift, typescript-eslint, AST Explorer, and the TSDoc parser to make further semantic guarantees of one’s codebase, and whenever possible, automate the fix with the handy eslint --fix command. The importance of static analysis cannot be emphasized enough! Bravo! Zk 10 Has Completely Migrated to TypeScript For ZK 10, we have actively undergone static analysis with TypeScript for all existing JavaScript code in our codebase. Not only were we able to fix existing errors (some are automatic with eslint --fix), thanks to the typescript-eslint project that enables lots of extra type-aware rules, we also wrote our own rules, and we are guaranteed to never make those mistakes ever again in the future. This means less mental burden and a better conscience for the ZK development team. Our Client MVVM effort also becomes much more manageable with TypeScript in place. The development experience is close to that of Java. In fact, some aspects are even better, as TypeScript has better type narrowing, structural typing, refinement types via literal types, and intersection/union types. As for our users, ZK 10 has become more reliable. Furthermore, our type definitions are freely available, so that ZK 10 users can customize the ZK frontend components with ease and confidence. In addition, users can scale their applications during execution with Client MVVM. Adopting TypeScript in ZK 10 further enables us to scale correctness during development. Both are fundamental improvements. Annex: Comparing Static Typing Solutions for JavaScript Google’s Closure Compiler Type system soundness unknown; Assumed as unsound, as sound type systems are rare @interface denotes nominal types whereas @record denotes structural types All type annotations are specified in comments leading to code bloat, and comments often go out of sync with the code. Most advanced and aggressive code optimization among all options listed here Find more information on GitHub Facebook’s Flow Unsound type system Nominal types for ES6 classes and structural types for everything else, unlike TypeScript where all types are structural; whereas in Java, all types are nominal Compared to TypeScript, Flow has a much smaller ecosystem in terms of tooling (compatible formatter, linter, IDE plugin) and libraries (TypeScript even has the DefinitelyTyped project to host type definitions on NPM) Find more information in Flow Documentation Microsoft’s TypeScript Supports all JavaScript features and follows the ECMA standard closely even for subtleties: class fields and TC39 decorators Seamless interoperation between all mainstream JavaScript module systems: ES modules, CommonJS, AMD, and UMD Unsound type system All types are structural, which is the most natural way to model dynamic types statically, but the ability to mark certain types as nominal would be good to have. Flow and the Closure Compiler have an edge in this respect. Also supports Closure-Compiler-style type annotations in comments Best-in-class tooling and a massive ecosystem; built-in support by VSCode; hence, its availability is almost ubiquitous Each enum variant is a separate subtype, unlike all other type systems we have ever encountered, including Rust, Scala 3, Lean 4, and Coq Find more information in The TypeScript Handbook Scala.js Leverages the awesome type system of Scala 3, which is sound Seamlessly shares build scripts (sbt) and code with any Scala 3 project The emitted JavaScript code is often bloated and sometimes less efficient than that of the Closure Compiler, Flow, and TypeScript. Learn more on the Scala.js site ReScript Touted to have a sound type system (where is the proof?) like that of Scala 3, but the syntax of ReScript is closer to JavaScript and OCaml The type system is highly regular like all languages in the ML family, allowing for efficient type checking, fast JavaScript emission, and aggressive optimizations. The emitted JavaScript code is very readable. This is a design goal of ReScript. Interoperation with TypeScript via genType As of ReScript 10.1, async/await is supported. Might require familiarity with more advanced functional programming techniques and purely functional data structures Learn more in the ReScript Language Manual documentation
We have recently released our new framework for ephemeral resources, which was the result of a big design effort, going from a deep analysis of the problem to the exploration of multiple solutions. In this article, we share the thinking process we underwent while designing this new framework, presenting some of the challenges we faced and the solutions we found and providing the reader with a clear idea about the resulting design. Before getting deep into the details, it is worth commenting that one of the goals we usually try to achieve when introducing changes in our Operator (the software that runs in the user's clusters) is to make them generic enough to allow us to deliver new user-facing features without requiring users to upgrade their operator version every time. Or, in other words, be able to release new features just by introducing changes in our control plane that runs in our cloud (and thus, we have full control over it). We believe it is important to explain this, as you may notice a general tendency in all of our designs and not understand the reasoning behind it. The Problem Our alpha version of resources, launched a year ago, was a hook-based framework that defined a container image interface where users had to implement two executables: one for provisioning the resource and another for deprovisioning it. Those custom resource images must be later registered in the corresponding clusters by creating a Resource Plugin CRD object and could finally be referenced from Sandboxes. Although this solution was generic and flexible, it proved to be too complex for the wider audience. The fact it required building and publishing custom images was a big obstacle for many of our users. Another observed downside was it didn't offer a clear separation of concerns between what belongs to the Dev vs. to the DevOps team. If you think in the context of Sandboxes, in many cases provisioning a resource is not only making it available but also configuring it to be functional in its context. For example, imagine a microservice that requires a database for reading and writing data. In that case, the resource would be the database, but most likely to be functional within a sandbox (be able to create a fork of the microservice using the provisioned resource), it would require to have a schema defined and maybe some initial data loaded. On one hand, it is clear that the provisioning of the database in itself is a DevOps responsibility (for example, the creation of an AWS RDS instance or the deployment of an in-cluster database). But on the other hand, the creation of the schema, and especially the definition of what seed data is required to properly work, is a Dev concern (for example, one solution could be loading a subset of the data from a staging database). Another important aspect to consider about the previous point is the rate of changes: while the provisioning logic tends to be very static once defined, the definition of the schema and seeding process may change much more frequently throughout the life-cycle of the microservice. So, in summary, putting these two concerns together within a single image required a tight collaboration between teams, which in many cases became a bottleneck, considerably decreasing the adoption of the resource framework. The problem to solve was clear, providing a new framework for ephemeral resources as generic and flexible as the first one but without the above downsides. The Design Right from the beginning of our design discussions, we all agreed the hook-based model was a good solution for this problem. Ultimately, all we needed was to give users the ability to run their own logic at different points throughout the lifecycle of a sandbox: before starting to create the resource and after termination to delete it. So, what we had to change was the kind of hook accepted by our framework but not the original approach. On the other hand, we still considered that container-based hooks were the right solution, or at least the first solution to begin with (this is, encapsulating the hook logic within a container that runs as a Kubernetes Pod). But we wanted it to be more declarative and less opinionated: instead of forcing users to build a custom image implementing a certain interface, let them specify what they want to run and how they want to do it. For example, the definition of a hook could look something like: YAML image: ubuntu namespace: signadot script: | #!/bin/bash echo "Provisioning my custom resource ..." That would be fairly simple for users to write and wouldn't require building any new images. Now, an idea we had in mind was: is this hook framework something entirely related to resources, or is this something we could very well use outside the context of resources? And yes, in our view, this framework went beyond the scope of resources and even sandboxes. A generic implementation would allow us, in the future, to run arbitrary hooks related to the lifecycle of entities like route groups, sandboxes, and others. And that, for sure, would be something good to support. Another topic of discussion was whether we should allow the execution of a single hook for each lifecycle event (preStart and postStop), or if it would be useful to support many. Sometimes it is hard to predict how users will use a given tool, especially in cases like this that are very much open-ended, but if we analyze the problem we previously explained about the separation of concerns between the DevOps and the Dev teams, we can see at least one case where being able to execute two different hooks as part of the same event would be beneficial. Based on the above, we decided to support multiple hooks per event. What led us to the next decision point, which is: how will those hooks execute? will it be in sequence? in parallel? following other patterns? To answer these questions, we should first focus on a very important concept, that is, the input/output (IO) between hooks and/or related entities. However, before proceeding, let's formalize our design a bit more. Let’s define an entity called ObjectLifecycleMethods (OLM) that will include a list of OLMWorkUnits for each of the tracked events (so far PreStart and PostStop, but this could be extended in the future if needed). Each OLMWorkUnits will define custom logic to be executed (these are the specific hooks the user wants to run for each event). We assume there is always going to be an object to which we are connecting our hooks, or in other words; our hooks will always be related to the lifecycle of one specific object (could it be a sandbox, a route group, or others). We will call this object the TargetObject. Then, when using hooks, a TargetObject will own an ObjectLifecycleMethods and will interact with it as needed. Now, to make this solution really functional, we should allow having IO between OLMWorkUnits. Without that, we would be imposing a big restriction that would drive the usability of this framework to barely just a few use cases. Based on the previous, let’s consider then that an OLMWorkUnit may consume inputs and produce outputs. But if we think about the data flow in the context of Sandboxes, we will see there could be IO across many different entities. As presented in the image above, OLMWorkUnits may: Receive inputs coming from the sandbox spec (the sandbox YAML). Receive inputs produced by other OLMWorkUnits, even from units across different events. Send parameters to the sandbox object. The ability to receive inputs from the sandbox specification provides a good level of parametrization at the resource definition level, which for sure could be very useful. Let’s now analyze a concrete example to see how all those things fit together. Imagine we need a resource to provision an AWS S3 bucket and seed it by uploading some required files for a microservice to run. We could split the creation of the resource into two different OLMWorkUnits, one that actually provisions the S3 bucket in AWS (a more DevOps-oriented task) and another that runs a script to seed the bucket (Dev will be in charge of this logic). It seems a good idea to receive the AWS region where the bucket will be created as a parameter from the sandbox spec (that way, we can use the same resource implementation across many AWS regions). The “Provision S3 Bucket” step will create the bucket using a random name and will return the bucket name as an output. Next, the “Seed S3 Bucket” step will depend on the “Provision S3 Bucket” and will receive the bucket name as input to upload the needed files. It will also generate an output with the prefix where the uploaded files were located. Now, if we are including this resource as part of a sandbox, it is because we are forking a service that requires it. For this forked service to use the ephemeral bucket, we will need to inject both the bucket name and base prefix as inputs. Finally, once the sandbox is terminated, the "Deprovision S3 Bucket” step will run, which will require the bucket name to remove from AWS. Having now this IO model clearly defined, we can go back to the question of how to execute OLMWorkUnits for one specific event. We concluded that the execution order of OLMWorkUnits can be defined by their IO dependencies. In the previous example, “Seed S3 Bucket” is run after “Provision S3 Bucket” because there is an IO dependency: the first requires an input produced by the second. In more technical terms, the OLMWorkUnits configured for one event will form a directed acyclic graph (DAG), where the edges correspond to IO relationships. That DAG will then be used to guide the execution of the different units by following this logic: each unit will be run as soon as all its dependencies have successfully completed or as soon as the event gets triggered in the case of no dependencies. In short, with this design, we will be executing a workflow for each of the implemented lifecycle events. Another aspect of the solution is how the ObjectLifecycleMethods relates to sandbox resources. One option we had was to make the Sandbox the owner of an OLM instance, who would be responsible for implementing the corresponding resource. However, we decided that introducing a new Resource entity would be a better and neater approach. In this design, the Resource is a user-facing entity that provides meaningful information in the context of resources, while the OLM is just an implementation detail. Apart from this, if in the future we keep extending the OLM, for example, by adding support for other life-cycle events like PostStart and PreStop, then not every OLM would be a fit as a Resource. This means that the OLM associated with a Resource could be a more shaped or constrained version than a regular one. Another advantage of this design is it would allow us the implementation of arbitrary hooks at the Sandbox level very easily (let the user run custom logic at different lifecycle events outside the context of resources) just by making the Sandbox have its own version of an OLM. And the same concept would apply to other entities like Route Groups. Before closing this section, let's analyze how we designed the OLMWorkUnit more deeply. We knew the first kind of hook we should implement was based on running containers. It was "simple," flexible, and powerful. But we could easily foresee other types of hooks which would make a whole lot of sense to support, such as webhooks and, more interestingly, the ability to create arbitrary Kubernetes objects. Imagine someone using Crossplane, where the creation of a resource would just mean creating a Crossplane CRD object, or someone running an Argo Workflow as one of the units. It would be great to support such cases. Of course, in those, the concept of inputs and outputs would still be relevant, but its materialization would be completely different compared to the one with containers. We never intended to exclude the possibility of supporting those hooks. As part of the original design of OLMWorkUnit, we considered the idea of supporting multiple kinds of units. We even considered the possibility of mixing them within the workflow defined for a lifecycle event. Conclusion We presented an in-detail view of the design of our new framework for ephemeral resources. We introduced the concept of a generic ObjectLifecycleMethods entity, the base implementation for the resource framework, that could easily be integrated with other entities, providing a rich hooking system. We also presented some of the extensions we could implement in the future, such as: adding support for other lifecycle events, integration with webhooks, Kubernetes native objects, etc. We hope you enjoyed reading this. If you would like to learn more, check out Signadot's Documentation or join our community Slack channel (you are very welcome there!).
Most of the time, we process data from a file so that we can manipulate it from memory. This data can be numeric, string, or a combination of both. In this article, I will discuss how to open a file for reading with the built-in function open() and the use of Pandas library to manipulate data in the file. This also includes reading the contents of a file line by line and saving the same to a list. Things To Learn in This Article Open the file for reading Read the contents of a file line by line Store the read lines into a list data type For loop List comprehension Readlines Readline Read file using pandas 1. Open the File for Reading Python's built-in function open() can be used to open a file for reading and writing. It is defined below based on the Python documentation. Python open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) These are the supported values for the mode. Here is an example script called main.py that will open the file countries.txt for reading. main.py Python with open('countries.txt', mode='r') as f: # other stuff An Alternative Method of Opening a File An alternative way of opening a file for reading is the following. Python f = open('countries.txt', mode='r') # other stuff f.close() You have to close the file object explicitly with close(). Anyhow use the with statement whenever possible, as the context manager will handle the entry and exit execution of the code; hence closing the file object close() is not needed. 2. Read the Contents of a File Line by Line Let us go back to our code in main.py. I will add a block of code that will read the contents of the file line by line and print it. main.py Python with open('countries.txt', mode='r') as f: for lines in f: line = lines.rstrip() # rstrip() will remove the newline character print(line) # print to console countries.txt Python Australia China Philippines Ensure that the main.py and countries.txt are on the same directory. That is because of the code above. In my case, they are in F:\Project\8thesource path. Execute the main.py from the command line. Python PS F:\Project\8thesource> python main.py output Python Australia China Philippines There we have it. We read countries.txt line by line using the open() function and file object manipulation. The first line printed was Australia, followed by China, and finally the Philippines. It is consistent according to the sequence of how they were written in countries.txt file. 3. Store the Read Lines Into a List Data Type Python has a popular data type called list that can store other object types or a combination of object types. A list of integers could be [1, 2, 3]. A list of strings could be ['one', 'two', 'three']. A list of integer and string could be [1, 'city', 45]. A list of lists could be [[1, 2], [4, 6]]. A list of tuples could be [(1, 2), ('a', 'b')]. A list of dictionaries could be [{'fruit': 'mango'}, {'count': 100}]. I will modify the main.py to store the read lines into a list. a) For Loop main.py Python data_list = [] # a list as container for read lines with open('countries.txt', mode='r') as f: for lines in f: line = lines.rstrip() # remove the newline character data_list.append(line) # add the line in the list print(data_list) The countries.txt is the file name. We open it for reading with symbol r. We use the for loop to read each line and save it to a list called data_list. After saving all the lines to a list via append method, the items in the list are then printed. After executing the main.py, we got the following output. output Python ['Australia', 'China', 'Philippines'] b) List Comprehension Another option to save the read lines into the list is by the use of list comprehension. It uses a for loop behind the scene and is more compact but not beginner-friendly. Python with open('countries.txt', mode='r') as f: data = [item.rstrip() for item in f] print(data) output Python ['Australia', 'China', 'Philippines'] c) Readlines Yet another option to save the read lines in a list is the method readlines(). Python with open('countries.txt', mode='r') as f: data = f.readlines() print(data) The output still has the newline character \n. output Python ['Australia\n', 'China\n', 'Philippines'] This newline character can be removed by reading each items on that list and strip it. The readlines method is not an ideal solution if the file is big. d) Readline Another option to save the read line is by the use of the readline method. Python data_list = [] with open('countries.txt', mode='r') as f: while True: line = f.readline() line = line.rstrip() # remove the newline character \n if line == '': break data_list.append(line) print(data_list) output Python ['Australia', 'China', 'Philippines'] 4. Read File Using Pandas For people aspiring to become data scientists, knowledge of processing files is a must. One of the tools that should be learned is the Pandas library. This can be used to manipulate data. It can read files, including popular csv or comma-separated values formatted file. Here is a sample scenario, we are given a capitals.csv file that contains the name of the country in the first column and the corresponding capital in the second column. Our job is to get a list of country and capital names. capitals.csv Python Country,Capital Australia,Canberra China,Beijing Philippines,Manila Japan,Tokyo For this particular job, it is better to use the Pandas library. The expected outputs are the country list [Australia, China, Philippines, Japan] and the capital list [Canberra, Beijing, Manila, Tokyo]. Let us create capitals.py to read the capitals.csv using Pandas. capitals.py Python """ requirements: pandas Install pandas with pip install pandas """ import pandas as pd # Build a dataframe based from the csv file. df = pd.read_csv('capitals.csv') print(df) command line Python PS F:\Project\8thesource> python capitals.py output Python Country Capital 0 Australia Canberra 1 China Beijing 2 Philippines Manila 3 Japan Tokyo Now we need to get the values in the Country and Capital columns and convert those to a list. Python import pandas as pd # Build a dataframe based from the csv file. df = pd.read_csv('capitals.csv') print(df) # Get the lists of country and capital names. country_names = df['Country'].to_list() capital_names = df['Capital'].to_list() Pandas is very smart about this. It easily gets the tasks that we are after. Now let us print those lists. Python import pandas as pd # Build a dataframe based from the csv file. df = pd.read_csv('capitals.csv') print(df) # Get the lists of country and capital names. country_names = df['Country'].to_list() capital_names = df['Capital'].to_list() # Print names print('Country names:') print(country_names) print('Capital names:') print(capital_names) output Python Country Capital 0 Australia Canberra 1 China Beijing 2 Philippines Manila 3 Japan Tokyo Country names: ['Australia', 'China', 'Philippines', 'Japan'] Capital names: ['Canberra', 'Beijing', 'Manila', 'Tokyo'] That is it. We got the country and capital names as lists. 5. Conclusion We use the built-in function open() to open and read the contents of a file and utilize the for loop to read it line by line, then save it to a list — a Python data type. There are also options, such as list comprehension, readlines and readline to save data into the list. Depending on the tasks and file given, we can use the Pandas library to process a csv file. For further reading, have a look at Python's built-in function open() and the very useful Pandas Python library.
If you're a Java developer who’s been longing for a more streamlined and intuitive way of querying databases, similar to the elegant LINQ constructs in C#, then you should check out the open-source library JPAStreamer. In this article, I will explore the powerful capabilities of this Java tool that brings a LINQ-like experience to your Hibernate queries. Before diving deeper into JPAStreamer, I’ll describe some desirable properties of LINQ. A LINQ (Language Integrated Query) is a C# expression that can transform data from a variety of data sources, spanning from simple linear data structures to databases. Here is a basic example from the LINQ documentation that operates on an array of integers to find arbitrary scores greater than 80: C# int[] scores = { 97, 92, 81, 60 }; IEnumerable<int> scoreQuery = from score in scores where score > 80 select score; foreach (int i in scoreQuery) { Console.Write(i + " "); // Output: 97 92 81 } As seen above, the LINQ API closely resembles SQL constructs, however, is type-safe to use in C# contexts. In Java, we can express the same operations in a fluent and intuitive manner using the Stream API introduced in Java 8: Java final int[] scores = {97, 92, 81, 60} Stream.of(scores) .filter(score -> score > 80) .foreach(System::out); Although not identical to SQL, many of the Java Stream operations have SQL siblings, e.g., filter (where), sorted (order), and map (select). Both LINQ and the Stream API emphasize a declarative style of programming, allowing developers to express what they want to achieve rather than specifying how to do it - an approach that enhances code readability and maintainability. So, Java Streams Is Equivalent to C# LINQ? No, it’s not that simple. Let’s say our scores represent historic results from a gaming app, and thus are stored in a database rather than an in-memory array. To motivate players, we want to present a list of all-time high scores. With LINQ to SQL, the input array can simply be replaced with a reference to an Object Model entity; i.e., the C# metamodel of the database. Upon execution, LINQ will automatically issue a native query to the database and is equipped with join operations and query comprehension syntax to write more complex queries. Java Streams, on the other hand, cannot be converted to SQL queries...or can they? Introducing JPAStreamer JPAStreamer is an open-source JPA extension that bridges the gap between Java Streams and database queries. Using a custom implementation of the Java Stream interface, JPAStreamer can automatically translate Streams to efficient database queries and supports join and projection constructs to cover more ground. Unlike LINQ to SQL, JPAStreamer is not a standalone ORM but instead uses an existing JPA provider such as Hibernate to manage the database metamodel consisting of standard JPA Entities. Going forward, I’ll assume the gaming application uses Hibernate to manage the historic game sessions. Past scores are represented in a Score table in the database, and our JPA entity model has an associated Score Entity as follows: Java @Entity @Table(name = "score", schema = "game-db") public class Score implements Serializable { public Score() {} @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "score_id", nullable = false, updatable = false, columnDefinition = "smallint(5)") private Integer scoreId; @Column(name = "value", nullable = false, columnDefinition = "varchar(255)") private Integer value; @Column(name = "date", nullable = false, columnDefinition = "timestamp") private LocalDateTime date; @ManyToOne @JoinColumn(name="username", nullable = false, updatable = false, insertable = false) private Player player; // Setters and getters are left out for clarity… } As seen above, the score can be described by an id, a value, the date of play, and an associated player. Once the JPA metamodel is in place, JPAStreamer can be installed by adding the Maven dependency below: XML <dependency> <groupId>com.speedment.jpastreamer</groupId> <artifactId>jpastreamer-core</artifactId> <version>3.0.1</version> </dependency> (For Gradle installation, read more here. If you are using the Quarkus ecosystem a JPAStreamer extension is available in the Quarkiverse.) Upon installation, the project needs to be rebuilt to trigger the generation of JPAStreamer's own metamodel. This process is completely automated and the output can be found in the target/generated-sources folder. In this case, JPAStreamer generates a Score$ and Player$ class that we can use to formulate predicates that can be interpreted by the JPAStreamer query optimizer. Sound fuzzy? Let’s get concrete by writing a query that will filter out the top 10 scores: Java JPAStreamer jpaStreamer = JPAStreamer.of(“game-db”); jpaStreamer.stream(Score.class) .sorted(Score$.value.reversed()) .limit(10) .foreach(System::out); JPAStreamer is instantiated with a single line of code, referencing the persistence unit by its imagined name “game-db”. The second line creates a stream from the JPA entity Score. The scores are then sorted from high to low and the top 10 results are selected. Much like with LINQ, the execution of the query is delayed until the terminal operation is called; in this case, the foreach operator that prints the scores. Most importantly, this Stream will not materialize all the scores in the database as it is automatically translated to a SQL query: Plain Text select score0_.score_id as score_id1_0_, score0_.value as value2_0_, score0_.date as date3_0_, score0_.username as username4_0_, from score score0_ order by score0_.value dsc limit ?, ? To give credit to the players on the high-score list we may also want to display the name of the player associated with each score. As can be seen in the Score entity above, it has a Many-to-One relation to the Player class. I will not show an imaginative JPA Player Entity, but each player is uniquely identified by their username and has a field for first and last name. We can obtain the name of the associated player by joining in the player for each of the scores. Using JPAStreamer means we need to update the Stream source to not only include Score entities but also the joint Player objects. This join is defined using a Stream Configuration as shown below. As before, we gather the top ten scores and map these entries to their respective players. Lastly, the players and their scores are printed in the console. Java JPAStreamer jpaStreamer = JPAStreamer.of("game-db"); Map<Score, Player> scoreMap = jpaStreamer.stream( StreamConfiguration.of(Score.class).joining(Score$.player)) .sorted(Score$.value.reversed()) .limit(10) .collect(toMap( Function.identity(), Score::getPlayer ) ); scoreMap.forEach( (s, p) -> System.out.format("%s %s: %s points\n", p.getFirstName(), p.getLastName(), s.getValue() ) ); Conclusion JPAStreamer brings the power of type-safe and fluid querying to the world of Java development, offering developers an intuitive and expressive way to interact with relational databases. With its declarative syntax, lazy evaluation, and composability, JPAStreamer bears striking similarities to C#'s LINQ library. By adopting JPAStreamer, Java developers can benefit from a concise and efficient approach to querying databases, ultimately leading to cleaner and more maintainable code. Resources Getting Started with JPAStreamer JPAStreamer Demo Repository
In this article, we will create a simple concurrent gRPC chat server application. We will use .NET Core, a cross-platform, open-source, and modular framework, to build our chat server application. We will cover the following topics: A brief introduction to gRPC. Setting up the gRPC environment and defining the service contract. Implementing the chat service and handling client requests. Handling multiple clients concurrently using asynchronous programming Broadcasting chat messages to all connected clients in the same room. By the end of this tutorial, you will have an understanding of how to use gRPC to build a chat server. What Is gRPC? gRPC is an acronym that stands for Google Remote Procedure Calls. It was initially developed by Google and is now maintained by the Cloud Native Computing Foundation (CNCF). gRPC allows you to connect, invoke, operate, and debug distributed heterogeneous applications as easily as making a local function call. gRPC uses HTTP/2 for transport, a contract-first approach to API development, protocol Buffers (protobuf) as the interface definition language as well as its underlying message interchange format. It can support four types of API (Unary RPC, Server streaming RPC, Client streaming RPC, and Bidirectional streaming RPC). You can read more about gRPC here. Getting Started: Before we start to write code, an installation of .NET core needs to be done, and make sure you have the following prerequisites in place: Visual Studio Code, Visual Studio, or JetBrains Rider IDE. .NET Core. gRPC .NET Protobuf Step 1: Create a gRPC Project From the Visual Studio or Command Line You can use the following command to create a new project. If successful, you should have it created in the directory you specify with the name 'ChatServer.' PowerShell dotnet new grpc -n ChatServerApp Open the project with your chosen editor. I am using visual studio for Mac. Step 2: Define the Protobuf Messages in a Proto File Protobuf Contract: Create .proto file named server.proto within the protos folder. The proto file is used to define the structure of the service, including the message types and the methods that the service supports. ProtoBuf syntax = "proto3"; option csharp_namespace = "ChatServerApp.Protos"; package chat; service ChatServer { // Bidirectional communication stream between client and server rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage); } //Client Messages: message ClientMessage { oneof content { ClientMessageLogin login = 1; ClientMessageChat chat = 2; } } message ClientMessageLogin { string chat_room_id = 1; string user_name = 2; } message ClientMessageChat { string text = 1; } //Server Messages message ServerMessage { oneof content { ServerMessageLoginSuccess login_success = 1; ServerMessageLoginFailure login_failure = 2; ServerMessageUserJoined user_joined = 3; ServerMessageChat chat = 4; } } message ServerMessageLoginFailure { string reason = 1; } message ServerMessageLoginSuccess { } message ServerMessageUserJoined { string user_name = 1; } message ServerMessageChat { string text = 1; string user_name = 2; } ChatServer defines the main service of our chat application, which includes a single RPC method called HandleCommunication. The method is used for bidirectional streaming between the client and the server. It takes a stream of ClientMessage as input and returns a stream of ServerMessage as output. ProtoBuf service ChatServer { // Bidirectional communication stream between client and server rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage); } ClientMessageLogin, which will be sent by the client, has two fields called chat_room_id and user_name. This message type is used to send login information from the client to the server. The chat_room_id field specifies the chat room that the client wants to join, while the user_name field specifies the username that the client wants to use in the chat room ProtoBuf message ClientMessageLogin { string chat_room_id = 1; string user_name = 2; } ClientMessageChat which will be used to send chat messages from the client to the server. It contains a single field text. ProtoBuf message ClientMessageChat { string text = 1; } ClientMessage defines the different types of messages that a client can send to the server. It contains a oneof field, which means that only one of the fields can be set at a time. if you use oneof, the generated C# code will contain an enumeration indicating which fields have been set. The field names are "login" and "chat"which corresponds to the ClientMessageLogin and ClientMessageChat messages respectively ProtoBuf message ClientMessage { oneof content { ClientMessageLogin login = 1; ClientMessageChat chat = 2; } } ServerMessageLoginFailure defines the message sent by the server to indicate that a client failed to log in to the chat room. The reason field specifies the reason for the failure. ProtoBuf message ServerMessageLoginFailure { string reason = 1; } ServerMessageLoginSuccess defines the message sent by the server to indicate that a client has successfully logged in to the chat room. It contains no fields and simply signals that the login was successful. When a client sends a ClientMessageLogin message, the server will respond with either a ServerMessageLoginSuccess message or a ServerMessageLoginFailure message, depending on whether the login was successful or not. If the login was successful, the client can then start to send ClientMessageChat messages to start chat messages. ProtoBuf message ServerMessageLoginSuccess { } Message ServerMessageUserJoined defines the message sent by the server to the client when a new user joins the chat room. ProtoBuf message ServerMessageUserJoined { string user_name = 1; } Message ServerMessageChat defines the message sent by the server to indicate that a new chat message has been received. The text field specifies the content of the chat message, and the user_name field specifies the username of the user who sent the message. ProtoBuf message ServerMessageChat { string text = 1; string user_name = 2; } Message ServerMessage defines the different types of messages that can be sent from the server to the client. It contains a oneof field named content with multiple options. The field names are "login_success," "login_failure," "user_joined," and "chat," which correspond to the ServerMessageLoginSuccess, ServerMessageLoginFailure, ServerMessageUserJoined, and ServerMessageChat messages, respectively. ProtoBuf message ServerMessage { oneof content { ServerMessageLoginSuccess login_success = 1; ServerMessageLoginFailure login_failure = 2; ServerMessageUserJoined user_joined = 3; ServerMessageChat chat = 4; } } Step 3: Add a ChatService Class Add a ChatService class that is derived from ChatServerBase(generated from the server.proto file using the gRPC codegen protoc). We then override the HandleCommunication method. The implementation of the HandleCommunication method will be responsible for handling the communication between the client and the server. C# public class ChatService : ChatServerBase { private readonly ILogger<ChatService> _logger; public ChatService(ILogger<ChatService> logger) { _logger = logger; } public override Task HandleCommunication(IAsyncStreamReader<ClientMessage> requestStream, IServerStreamWriter<ServerMessage> responseStream, ServerCallContext context) { return base.HandleCommunication(requestStream, responseStream, context); } } Step 4: Configure gRPC In program.cs file: C# using ChatServer.Services; using Microsoft.AspNetCore.Server.Kestrel.Core; var builder = WebApplication.CreateBuilder(args); /* // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, // visit https://go.microsoft.com/fwlink/?linkid=2099682 To avoid missing ALPN support issue on Mac. To work around this issue, configure Kestrel and the gRPC client to use HTTP/2 without TLS. You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption. https://learn.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-7.0 */ builder.WebHost.ConfigureKestrel(options => { // Setup a HTTP/2 endpoint without TLS. options.ListenLocalhost(50051, o => o.Protocols = HttpProtocols.Http2); }); // Add services to the container. builder.Services.AddGrpc(); builder.Services.AddSingleton<ChatRoomService>(); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<ChatService>(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); Console.WriteLine($"gRPC server about to listening on port:50051"); app.Run(); Note: ASP.NET Core gRPC template and samples use TLS by default. But for development purposes, we configure Kestrel and the gRPC client to use HTTP/2 without TLS. Step 5: Create a ChatRoomService and Implement Various Methods Needed in HandleCommunication The ChatRoomService class is responsible for managing chat rooms and clients, as well as handling messages sent between clients. It uses a ConcurrentDictionary to store chat rooms and a list of ChatClient objects for each room. The AddClientToChatRoom method adds a new client to a chat room, and the BroadcastClientJoinedRoomMessage method sends a message to all clients in the room when a new client joins. The BroadcastMessageToChatRoom method sends a message to all clients in a room except for the sender of the message. The ChatClient class contains a StreamWriter object for writing messages to the client, as well as a UserName property for identifying the client. C# using System; using ChatServer; using Grpc.Core; using System.Collections.Concurrent; namespace ChatServer.Services { public class ChatRoomService { private static readonly ConcurrentDictionary<string, List<ChatClient>> _chatRooms = new ConcurrentDictionary<string, List<ChatClient>>(); /// <summary> /// Read a single message from the client. /// </summary> /// <exception cref="ConnectionLostException"></exception> /// <exception cref="TimeoutException"></exception> public async Task<ClientMessage> ReadMessageWithTimeoutAsync(IAsyncStreamReader<ClientMessage> requestStream, TimeSpan timeout) { CancellationTokenSource cancellationTokenSource = new(); cancellationTokenSource.CancelAfter(timeout); try { bool moveNext = await requestStream.MoveNext(cancellationTokenSource.Token); if (moveNext == false) { throw new Exception("connection dropped exception"); } return requestStream.Current; } catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled) { throw new TimeoutException(); } } /// <summary> /// <summary> /// </summary> /// <param name="chatRoomId"></param> /// <param name="user"></param> /// <returns></returns> public async Task AddClientToChatRoom(string chatRoomId, ChatClient chatClient) { if (!_chatRooms.ContainsKey(chatRoomId)) { _chatRooms[chatRoomId] = new List<ChatClient> { chatClient }; } else { var existingUser = _chatRooms[chatRoomId].FirstOrDefault(c => c.UserName == chatClient.UserName); if (existingUser != null) { // A user with the same user name already exists in the chat room throw new InvalidOperationException("User with the same name already exists in the chat room"); } _chatRooms[chatRoomId].Add(chatClient); } await Task.CompletedTask; } /// <summary> /// Broad client joined the room message. /// </summary> /// <param name="userName"></param> /// <param name="chatRoomId"></param> /// <returns></returns> public async Task BroadcastClientJoinedRoomMessage(string userName, string chatRoomId) { if (_chatRooms.ContainsKey(chatRoomId)) { var message = new ServerMessage { UserJoined = new ServerMessageUserJoined { UserName = userName } }; var tasks = new List<Task>(); foreach (var stream in _chatRooms[chatRoomId]) { if (stream != null && stream != default) { tasks.Add(stream.StreamWriter.WriteAsync(message)); } } await Task.WhenAll(tasks); } } /// <summary> /// </summary> /// <param name="chatRoomId"></param> /// <param name="senderName"></param> /// <param name="text"></param> /// <returns></returns> public async Task BroadcastMessageToChatRoom(string chatRoomId, string senderName, string text) { if (_chatRooms.ContainsKey(chatRoomId)) { var message = new ServerMessage { Chat = new ServerMessageChat { UserName = senderName, Text = text } }; var tasks = new List<Task>(); var streamList = _chatRooms[chatRoomId]; foreach (var stream in _chatRooms[chatRoomId]) { //This senderName can be something of unique Id for each user. if (stream != null && stream != default && stream.UserName != senderName) { tasks.Add(stream.StreamWriter.WriteAsync(message)); } } await Task.WhenAll(tasks); } } } public class ChatClient { public IServerStreamWriter<ServerMessage> StreamWriter { get; set; } public string UserName { get; set; } } } Step 6: Finally, Implement the gRPC HandleCommunication Method in Step 3 The HandleCommunication receives a requestStream from the client and sends a responseStream back to the client. The method reads a message from the client, extracts the username and chatRoomId, and handles two cases: a login case and a chat case. In the login case, the method checks if the username and chatRoomId are valid and sends a response message to the client accordingly. If the login is successful, the client is added to the chat room, and a broadcast message is sent to all clients in the chat room. In the chat case, the method broadcasts the message to all clients in the chat room. C# using System; using ChatServer; using Grpc.Core; namespace ChatServer.Services { public class ChatService : ChatServer.ChatServerBase { private readonly ILogger<ChatService> _logger; private readonly ChatRoomService _chatRoomService; public ChatService(ChatRoomService chatRoomService, ILogger<ChatService> logger) { _chatRoomService = chatRoomService; _logger = logger; } public override async Task HandleCommunication(IAsyncStreamReader<ClientMessage> requestStream, IServerStreamWriter<ServerMessage> responseStream, ServerCallContext context) { var userName = string.Empty; var chatRoomId = string.Empty; while (true) { //Read a message from the client. var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan); switch (clientMessage.ContentCase) { case ClientMessage.ContentOneofCase.Login: var loginMessage = clientMessage.Login; //get username and chatRoom Id from clientMessage. chatRoomId = loginMessage.ChatRoomId; userName = loginMessage.UserName; if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId)) { //Send a login Failure message. var failureMessage = new ServerMessage { LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" } }; await responseStream.WriteAsync(failureMessage); return; } //Send login succes message to client var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() }; await responseStream.WriteAsync(successMessage); //Add client to chat room. await _chatRoomService.AddClientToChatRoom(chatRoomId, new ChatClient { StreamWriter = responseStream, UserName = userName }); break; case ClientMessage.ContentOneofCase.Chat: var chatMessage = clientMessage.Chat; if (userName is not null && chatRoomId is not null) { //broad cast the message to the room await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text); } break; } } } } } Complete project directory: That is all for part 1. In the next part 2, I will create a client project with the client implementation to complete this chat application.
There are strong reasons why Node.js is so popular nowadays. Async programming model, cross-platform with a large community and open-source modules. It’s possible to write server-side on JavaScript, and it is fast and efficient. Node.js is designed to handle multiple asynchronous I/O operations efficiently. Node.js apply different execution types, and this article will explain how execution types work with practical usage in Node.js. Types of Execution V8 engine compiles JavaScript code into machine code that will be performed by the computer’s processor, which can have multiple cores. Node.js has only one main event loop thread, a mechanism used to handle async and non-blocking I/O operations. Non-blocking means an asynchronous programming paradigm that allows you to perform several operations at the same time without blocking the execution of the main program. This approach allows Node.js to efficiently process a large number of concurrent requests without losing resources on blocking I/O operations. The event loop works differently depending on the type of execution. Executions are stacked up, but there are several different types of execution, and here we will go through all of the execution type models in Node.js. There are three types of execution models in Node.js: Sequential, Concurrent, and Parallelism. The difference between those execution types is that each task takes some time to complete. 1. Sequential Execution This type has multiple task execution, which happens one after another, and the current task execution must be completed before moving to the next task. This model is helpful for minor services with a small number of cheap functions where the order of execution is important. Sequential Execution Schema Here is the simplest example of sequential execution. Each task is console.log and executed one after another in a sequence chain. JavaScript function executeAllTasks(): void { console.log('Task Execution 1'); console.log('Task Execution 2'); console.log('Task Execution 3'); } executeAllTasks(); Or with a sequence of functions where each function will be in the chain and executed one after another. JavaScript function executionTask1() { console.log('Execution Task 1'); } function executionTask2() { console.log('Execution Task 2'); } function executionTask3() { console.log('Execution Task 3'); } function executeAllTasks() { executionTask1(); executionTask2(); executionTask3(); } executeAllTasks(); The same idea with an async sequential execution. JavaScript function executionTask1(cb) { setTimeout(() => { console.log('Execution of Task 1'); cb(); }, 800); } function executionTask2(cb) { setTimeout(() => { console.log('Execution of Task 2'); cb(); }, 1500); } function executionTask3(cb) { setTimeout(() => { console.log('Execution of Task 3'); cb(); }, 300); } function main() { executionTask1(() => { executionTask2(() => { executionTask3(() => { console.log('All tasks executed'); }); }); }); } main(); This example shows how each task executes asynchronously using the setTimeout(). Each task will have a different time of execution. The callback will notify that the task was completed and the sequence is ready for the next task. The next example is more realistic. Let’s say we need to read data from a database, then update and delete. JavaScript import { Client } from 'pg'; const client = new Client({ user: 'username', host: 'hostname', database: 'database-name', password: 'password', port: 5432, }); async function runSequentially() { const postId = 1; const title = 'New Title'; try { await client.connect(); const res1 = await client.query('SELECT * FROM posts WHERE id = $1', [postId]); console.log(res1.rows[0]); const res2 = await client.query('UPDATE posts SET title = $1 WHERE id = $2 RETURNING *', [title, postId]); console.log(res2.rows[0]); const res3 = await client.query('DELETE FROM posts WHERE id = $1', [2]); console.log(res3.rowCount); await client.end(); } catch (err) { console.error(err); } } runSequentially(); From the chain of dependencies, we expect results that can be used in the following tasks. 2. Concurrent This is an execution of tasks in overlapping time intervals. Tasks can start and run simultaneously, but completion can be at different times. Concurrent Execution Schema In a concurrent execution model, tasks can be executed independently of one another without waiting to complete the previous task. It can be helpful for applications that have several functions that can run independently. JavaScript const fs = require('fs'); fs.readFile('fileOne.txt', (error, data) => { console.log(data.toString()); }); fs.readFile('fileTwo.txt', (error, data) => { console.log(data.toString()); }); fs.readFile('fileThree.txt', (error, data) => { console.log(data.toString()); }); As you can see, all files are read concurrently, and the console logs will display the data of each file as soon as it’s read. Each task will start at the same time but could take different amounts of time to complete, and they may not complete at the same time. To illustrate it with a more realistic example, let’s imagine that we have a function downloadFile but we have to deal with different files and download all of them. JavaScript const https = require('https'); const fs = require('fs'); interface FileProps { url: string; path: string; } async function downloadFile({ url, path }: FileProps): Promise<void> { const file = fs.createWriteStream(path); return new Promise<void>((resolve, reject) => { https .get(url, (response) => { response.pipe(file); file.on("finish", () => { file.close(); resolve(); }); }) .on("error", (error) => { fs.unlink(path); reject(error); }); }); } Now, let’s try to use downloadFile in concurrent example: JavaScript async function downloadFiles(): Promise<void> { await Promise.all( [ { url: "https://example.com/file1.xml", path: "file1.xml" }, { url: "https://example.com/file2.xml", path: "file2.xml" }, { url: "https://example.com/file3.xml", path: "file3.xml" }, ].map(downloadFile) ); } downloadFiles(); Function downloadFiles downloads multiple files concurrently by creating an array of promises. By downloading files concurrently, the program can do that in a shorter way, making the program more efficient. 3. Parallelism This type splits each task execution into smaller tasks and executes them simultaneously on different processors or cores. This type of execution allows completing all the tasks faster than executing them sequentially or concurrently. Parallelism Execution Schema This type of execution needs to care about the number of CPU cores, but parallel execution can significantly improve performance for applications that have CPU-bound tasks, like mathematical calculations, data processing, and image resizing or video encoding/decoding. JavaScript const workerThreads = require("worker_threads"); function runService(task) { return new Promise((resolve, reject) => { const worker = new workerThreads.Worker("./index.js", { workerData: task }); worker.on("message", resolve); worker.on("error", reject); worker.on("exit", (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } }); }); } async function runParallel(tasks) { return Promise.all(tasks.map(runService)); } runParallel(["task1", "task2", "task3"]).catch(console.error); In this case, the function runService creates three workers that execute in parallel. JavaScript import sharp from "sharp"; import { Worker, isMainThread, parentPort, workerData } from "worker_threads"; interface ImageResizeTask { id: number; input: string; output: string; width: number; height: number; } async function resizeImage(task: ImageResizeTask): Promise<void> { await sharp(task.input).resize(task.width, task.height).toFile(task.output); if (isMainThread) { console.log(`Task completed: ${task.input} -> ${task.output}`); } else { parentPort!.postMessage({ id: task.id }); } } async function runParallel(tasks: ImageResizeTask[]): Promise<void> { await Promise.all( tasks.map((task, index) => { return new Promise<void>((resolve, reject) => { const worker = new Worker(__filename, { workerData: task, }); worker.on("message", () => { resolve(); }); worker.on("error", reject); worker.on("exit", (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } }); }); }) ); } if (isMainThread) { runParallel([ { id: 1, input: "input1.png", output: "output1.png", width: 300, height: 200, }, { id: 2, input: "input2.png", output: "output2.png", width: 800, height: 600, }, { id: 3, input: "input3.png", output: "output3.png", width: 1024, height: 768, }, ]); } else { resizeImage(workerData); } In this example, the resizeImage() function uses the sharp lib to resize the image and save it to the output file. It will output a message indicating that the task has been completed if the code is executed in the main thread. Running on a worker thread will send a message to the main thread indicating that the task has been completed. Node.js supports parallelism through worker threads. It allows us to execute code in separate threads, and each has its own call stack and memory because it runs in different CPU cores. Conclusion Each type of execution has to have a specific use case and requirements of the program. Using those types in Node.js can improve performance, scalability, and efficiency depending on what you build. By choosing the right execution type, you can optimize the performance of your application.