<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Grab - Medium]]></title>
        <description><![CDATA[Grab’s Latest Stories - Medium]]></description>
        <link>https://medium.com/grab?source=rss----583648db97e0---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Grab - Medium</title>
            <link>https://medium.com/grab?source=rss----583648db97e0---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 13:07:11 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/grab" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Processing ETL Tasks with Ratchet]]></title>
            <link>https://medium.com/grab/processing-etl-tasks-with-ratchet-36f8a4cf0c95?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/36f8a4cf0c95</guid>
            <category><![CDATA[pipeline]]></category>
            <category><![CDATA[etl]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Mon, 19 Jul 2021 10:41:03 GMT</pubDate>
            <atom:updated>2021-07-27T02:17:02.439Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Amar Prakash</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*te-tUYss4gidagEDuruHow.jpeg" /></figure><h3>Overview</h3><p>At Grab, the Lending team is focused towards building products that help finance various segments of users, such as Passengers, Drivers, or Merchants, based on their needs. The team builds products that enable users to avail funds in a seamless and hassle-free way. In order to achieve this, multiple lending microservices continuously interact with each other. Each microservice handles different responsibilities, such as providing offers, storing user information, disbursing availed amounts to a user’s account, and many more.</p><p>In this tech blog, we will discuss what <em>Data</em> and <em>Extract, Transform and Load (ETL)</em> pipelines are and how they are used for processing multiple tasks in the Lending Team at Grab. We will also discuss <a href="https://github.com/dailyburn/ratchet"><em>Ratchet</em></a>, which is a Go library, that helps us in building data pipelines and handling ETL tasks. Let’s start by covering the basis of Data and ETL pipelines.</p><h3>What is a Data Pipeline?</h3><p>A Data pipeline is used to describe a system or a process that moves data from one platform to another. In between platforms, data passes through multiple steps based on defined requirements, where it may be subjected to some kind of modification. All the steps in a Data pipeline are automated, and the output from one step acts as an input for the next step.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*n0W5hyiWAM1i_fan.png" /><figcaption><em>Data Pipeline (Source: </em><a href="https://hazelcast.com/glossary/data-pipeline/"><em>Hazelcast</em></a><em>)</em></figcaption></figure><h3>What is an ETL Pipeline?</h3><p>An ETL pipeline is a type of Data pipeline that consists of 3 major steps, namely extraction of data from a source, transformation of that data into the desired format, and finally loading the transformed data to the destination. The destination is also known as the <em>sink</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xKuhwWSD_QazqYhC.jpg" /><figcaption><em>Extract-Transform-Load (Source: </em><a href="https://www.tatvasoft.com/blog/etl-process-extract-transform-load/"><em>TatvaSoft</em></a><em>)</em></figcaption></figure><p>The combination of steps in an ETL pipeline provides functions to assure that the business requirements of the application are achieved.</p><p>Let’s briefly look at each of the steps involved in the ETL pipeline.</p><h3>Data Extraction</h3><p>Data extraction is used to fetch data from one or multiple sources with ease. The source of data can vary based on the requirement. Some of the commonly used data sources are:</p><ul><li>Database</li><li>Web-based storage (S3, Google cloud, etc)</li><li>Files</li><li>User Feeds, CRM, etc.</li></ul><p>The data format can also vary from one use case to another. Some of the most commonly used data formats are:</p><ul><li>SQL</li><li>CSV</li><li>JSON</li><li>XML</li></ul><p>Once data is extracted in the desired format, it is ready to be fed to the transformation step.</p><h3>Data Transformation</h3><p>Data transformation involves applying a set of rules and techniques to convert the extracted data into a more meaningful and structured format for use. The extracted data may not always be ready to use. In order to transform the data, one of the following techniques may be used:</p><ol><li>Filtering out unnecessary data.</li><li>Preprocessing and cleaning of data.</li><li>Performing validations on data.</li><li>Deriving a new set of data from the existing one.</li><li>Aggregating data from multiple sources into a single uniformly structured format.</li></ol><h3>Data Loading</h3><p>The final step of an ETL pipeline involves moving the transformed data to a sink where it can be accessed for its use. Based on requirements, a sink can be one of the following:</p><ol><li>Database</li><li>File</li><li>Web-based storage (S3, Google cloud, etc)</li></ol><p>An ETL pipeline may or may not have a loadstep based on its requirements. When the transformed data needs to be stored for further use, the loadstep is used to move the transformed data to the storage of choice. However, in some cases, the transformed data may not be needed for any further use and thus, the loadstep can be skipped.</p><p>Now that you understand the basics, let’s go over how we, in the Grab Lending team, use an ETL pipeline.</p><h3>Why Use Ratchet?</h3><p>At Grab, we use Golang for most of our backend services. Due to Golang’s simplicity, execution speed, and concurrency support, it is a great choice for building data pipeline systems to perform custom ETL tasks.</p><p>Given that <a href="https://github.com/dailyburn/ratchet"><em>Ratchet</em></a> is also written in Go, it allows us to easily build custom data pipelines.</p><p>Go channels are connecting each stage of processing, so the syntax for sending data is intuitive for anyone familiar with Go. All data being sent and received is in JSON, providing a nice balance of flexibility and consistency.</p><h3>Utilising Ratchet for ETL Tasks</h3><p>We use Ratchet for multiple ETL tasks like batch processing, restructuring and rescheduling of loans, creating user profiles, and so on. One of the backend services, named <strong>Azkaban</strong>, is responsible for handling various ETL tasks.</p><p>Ratchet uses <em>Data Processors</em> for building a pipeline consisting of multiple stages. Data Processors each run in their own goroutine so all of the data is processed concurrently. Data Processors are organised into stages, and those stages are run within a pipeline. For building an ETL pipeline, each of the three steps (Extract, Transform and Load) use a Data Processor for implementation. Ratchet provides a set of built-in, useful Data Processors, while also providing an interface to implement your own. Usually, the transform stage uses a Custom Data Processor.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/664/0*sa4CrOM8QrKL72o7.png" /><figcaption><em>Data Processors in Ratchet (Source: </em><a href="https://github.com/dailyburn/ratchet"><em>Github</em></a><em>)</em></figcaption></figure><p>Let’s take a look at one of these tasks to understand how we utilise Ratchet for processing an ETL task.</p><h3>Whitelisting Merchants Through ETL Pipelines</h3><p>Whitelisting essentially means making the product available to the user by mapping an offer to the user ID. If a merchant in Thailand receives an option to opt for Cash Loan, it is done by whitelisting that merchant. In order to whitelist our merchants, our Operations team uses an internal portal to upload a CSV file with the user IDs of the merchants and other required information. This CSV file is generated by our internal Data and Risk team and handed over to the Operations team. Once the CSV file is uploaded, the user IDs present in the file are whitelisted within minutes. However, a lot of work goes in the background to make this possible.</p><h3>Data Extraction</h3><p>Once the Operations team uploads the CSV containing a list of merchant users to be whitelisted, the file is stored in S3 and an entry is created on the Azkaban service with the document ID of the uploaded file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/759/0*y2tsBX3kjArPWgmw.png" /><figcaption><em>File upload by Operations team</em></figcaption></figure><p>The data extraction step makes use of a Custom CSV Data Processor that uses the document ID to first create a PreSignedUrl and then uses it to fetch the data from S3. The data extracted is in bytes and we use commas as the delimiter to format the CSV data.</p><h3>Data Transformation</h3><p>In order to transform the data, we define a Custom Data Processor that we call a <em>Transformer</em> for each ETL pipeline. Transformers are responsible for applying all necessary transformations to the data before it is ready for loading. The transformations applied in the merchant whitelisting transformers are:</p><ol><li>Convert data from bytes to struct.</li><li>Check for presence of all mandatory fields in the received data.</li><li>Perform validation on the data received.</li><li>Make API calls to external microservices for whitelisting the merchant.</li></ol><p>As mentioned earlier, the CSV file is uploaded manually by the Operations team. Since this is a manual process, it is prone to human errors. Validation of data in the data transformation step helps avoid these errors and not propagate them further up the pipeline. Since CSV data consists of multiple rows, each row passes through all the steps mentioned above.</p><h3>Data Loading</h3><p>Whenever the merchants are whitelisted, we don’t need to store the transformed data. As a result, we don’t have a loadstep for this ETL task, so we just use an Empty Data Processor. However, this is just one of many use cases that we have. In cases where the transformed data needs to be stored for further use, the loadstep will have a Custom Data Processor, which will be responsible for storing the data.</p><h3>Connecting All Stages</h3><p>After defining our Data Processors for each of the steps in the ETL pipeline, the final piece is to connect all the stages together. As stated earlier, the ETL tasks have different ETL pipelines and each ETL pipeline consists of 3 stages defined by their Data Processors.</p><p>In order to connect these 3 stages, we define a <strong>Job Processor</strong> for each ETL pipeline. A Job Processor represents the entire ETL pipeline and encompasses Data Processors for each of the 3 stages. Each Job Processor implements the following methods:</p><ol><li>SetSource: Assigns the Data Processor for the Extraction stage.</li><li>SetTransformer: Assigns the Data Processor for the Transformation stage.</li><li>SetDestination: Assigns the Data Processor for the Load stage.</li><li>Execute: Runs the ETL pipeline.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MeknptKdWVqBnm5F.png" /><figcaption><em>Job processors containing Data Processor for each stage in ETL</em></figcaption></figure><p>When the <strong>Azkaban</strong> service is initialised, we run the SetSource(), SetTransformer() and SetDestination() methods for each of the Job Processors defined. When an ETL task is triggered, the Execute() method of the corresponding Job Processor is run. This triggers the ETL pipeline and gradually runs the 3 stages of ETL pipeline. For each stage, the Data Processor assigned during initialisation is executed.</p><h3>Conclusion</h3><p>ETL pipelines help us in streamlining various tasks in our team. As showcased through the example in the above section, an ETL pipeline breaks a task into multiple stages and divides the responsibilities across these stages.</p><p>In cases where a task fails in the middle of the process, ETL pipelines help us determine the cause of the failure quickly and accurately. With ETL pipelines, we have reduced the manual effort required for validating data at each step and avoiding propagation of errors towards the end of the pipeline.</p><p>Through the use of ETL pipelines and schedulers, we at Lending have been able to automate the entire pipeline for many tasks to run at scheduled intervals without any manual effort involved at all. This has helped us tremendously in reducing human errors, increasing the throughput of the system and making the backend flow more reliable. As we continue to automate more and more of our tasks that have tightly defined stages, we foresee a growth in our ETL pipelines usage.</p><h3>References</h3><p><a href="https://www.alooma.com/blog/what-is-a-data-pipeline">https://www.alooma.com/blog/what-is-a-data-pipeline</a></p><p><a href="http://rkulla.blogspot.com/2016/01/data-pipeline-and-etl-tasks-in-go-using.html">http://rkulla.blogspot.com/2016/01/data-pipeline-and-etl-tasks-in-go-using</a></p><p><a href="https://medium.com/swlh/etl-pipeline-and-data-pipeline-comparison-bf89fa240ce9">ETL Pipeline and Data Pipeline Comparison</a></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=36f8a4cf0c95" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/processing-etl-tasks-with-ratchet-36f8a4cf0c95">Processing ETL Tasks with Ratchet</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[App Modularisation at Scale]]></title>
            <link>https://medium.com/grab/app-modularisation-at-scale-ff2d7eded41f?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/ff2d7eded41f</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[monorepo]]></category>
            <category><![CDATA[apps]]></category>
            <category><![CDATA[build-time]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Wed, 14 Jul 2021 04:09:37 GMT</pubDate>
            <atom:updated>2021-07-14T04:09:37.078Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Amar Jain</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fo6DCvvIhrrKUKumUylNWA.jpeg" /></figure><p>Grab a coffee ☕️, sit back and enjoy reading. 😃</p><p>Wanna know how we improved our app’s build time performance and developer experience at Grab? Continue reading…</p><h3>Where it all began</h3><p>Imagine you are working on an app that grows continuously as more and more features are added to it, it becomes challenging to manage the code at some point. Code conflicts increase due to coupling, development slows down, releases take longer to ship, collaboration becomes difficult, and so on.</p><p>Grab superapp is one such app that offers many services like booking taxis, ordering food, payments using an e-wallet, transferring money to friends/families, paying at merchants, and many more, across Southeast Asia.</p><p>Grab app followed a monolithic architecture initially where the entire code was held in a single module containing all the UI and business logic for almost all of its features. But as the app grew, new developers were hired, and more features were built, it became difficult to work on the codebase. We had to think of better ways to maintain the codebase, and that’s when the team decided to modularise the app to solve the issues faced.</p><h3>What is Modularisation?</h3><p>Breaking the monolithic app module into smaller, independent, and interchangeable modules to segregate functionality so that every module is responsible for executing a specific functionality and will contain everything necessary to execute that functionality.</p><p>Modularising the Grab app was not an easy task as it brought many challenges along with it because of its complicated structure due to the high amount of code coupling.</p><h3>Approach and Design</h3><p>We divided the task into the following sub-tasks to ensure that only one out of many functionalities in the app was impacted at a time.</p><ul><li>Setting up the infrastructure by creating Base/Core modules for Networking, Analytics, Experimentation, Storage, Config, and so on.</li><li>Building Shared Library modules for Styling, Common-UI, Utils, etc.</li><li>Incrementally building Feature modules for user-facing features like Payments Home, Wallet Top Up, Peer-to-Merchant (P2M) Payments, GrabCard and many others.</li><li>Creating Kit modules for the feature to feature module communication. This step helped us in building the feature modules in parallel.</li><li>Finally, the App module is used as a hub to connect all the other modules together using dependency injection (Dagger).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/641/0*YpZlOE4XPangJVxO.png" /><figcaption><em>Modularised app structure</em></figcaption></figure><p>In the above diagram, <em>payments-home</em>, <em>wallet top-up</em>, and <em>grabcard</em> are different features provided by the Grab app. <em>top-up-kit</em> and <em>grabcard-kit</em> are bridges that expose functionalities from <em>topup</em> and <em>grabcard</em> modules to the payments-home module, respectively.</p><p>In the process of modularising the Grab app, we ensured that a feature module did not directly depend on other feature modules so that they could be built in parallel using the available CPU cores of the machine, hence reducing the overall build time of the app.</p><p>With the <em>Kit</em> module approach, we separated our code into independent layers by depending only on abstractions instead of concrete implementation.</p><h3>Modularisation Benefits</h3><ul><li><strong>Faster build times and hence faster CI</strong>: Gradle build system compiles only the changed modules and uses the binaries of all the non-affected modules from its cache. So the compilation becomes faster. Moreover, independent modules are run in parallel on different threads.</li><li><strong>Fine dependency graph</strong>: Dependencies of a module are well defined.</li><li><strong>Reusability across other apps</strong>: Modules can be used across different apps by converting them into an AAR SDK.</li><li><strong>Scale and maintainability</strong>: Teams can work independently on the modules owned by them without blocking each other.</li><li><strong>Well-defined code ownership</strong>: Clear responsibility on who owns which code.</li></ul><h3>Limitations</h3><ul><li>Requires more effort and time to modularise an app.</li><li>Separate configuration files to be maintained for each module.</li><li>Gradle sync time starts to grow.</li><li>IDE becomes very slow and its memory usage goes up a lot.</li><li>Parallel execution of the module depends on the machine’s capabilities.</li></ul><h3>Where we are now</h3><p>There are more than 1,000 modules in the Grab app and are still counting.</p><p>At Grab, we have many sub-teams which take care of different features available in the app. Grab Financial Group (GFG) is one such sub-team that handles everything related to payments in the app. For example: P2P &amp; P2M money transfers, e-Wallet activation, KYC, and so on.</p><p>We started modularising payments further in July 2020 as it was already bombarded with too many features and it was difficult for the team to work on the single payments module. The result of payments modularisation is shown in the following chart.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*S91RbZ0ASmHSVNPw.png" /><figcaption><em>Build time graph of payments module</em></figcaption></figure><p>As of today, we have about 200+ modules in GFG and more than 95% of the modules take less than 15s to build.</p><h3>Conclusion</h3><p>Modularisation has helped us a lot in reducing the overall build time of the app and also, in improving the developer experience by breaking dependencies and allowing us to define code ownership. Having said that, modularisation is not an easy or a small task, especially for large projects with legacy code. However, with careful planning and the right design, modularisation can help in forming a well-structured and maintainable project.</p><p>Hope you enjoyed reading. Don’t forget to 👏.</p><p>References:</p><ul><li><a href="https://proandroiddev.com/build-a-modular-android-app-architecture-25342d99de82">https://proandroiddev.com/build-a-modular-android-app-architecture-25342d99de82</a></li><li><a href="https://medium.com/google-developer-experts/modularizing-android-applications-9e2d18f244a0">https://medium.com/google-developer-experts/modularizing-android-applications-9e2d18f244a0</a></li><li><a href="https://medium.com/@mydogtom/modularization-part-1-application-structure-overview-9e465909a9bc">https://medium.com/@mydogtom/modularization-part-1-application-structure-overview-9e465909a9bc</a></li></ul><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff2d7eded41f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/app-modularisation-at-scale-ff2d7eded41f">App Modularisation at Scale</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Reshaping Chat Support for Our Users]]></title>
            <link>https://medium.com/grab/reshaping-chat-support-for-our-users-5ad2fc50f366?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/5ad2fc50f366</guid>
            <category><![CDATA[chat-support]]></category>
            <category><![CDATA[product]]></category>
            <category><![CDATA[design]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Wed, 14 Jul 2021 04:04:43 GMT</pubDate>
            <atom:updated>2021-07-14T04:04:42.898Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Elisa Monacchi, Wan Ling Guai, Suman Anand</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*mJ9O-4cCwQlx0-QQkbJS7w.png" /></figure><h3>Introduction</h3><p>The Grab support team plays a key role in ensuring our users receive support when things don’t go as expected or whenever there are questions on our products and services.</p><p>In the past, when users required real-time support, their only option was to call our hotline and wait in the queue to talk to an agent. But voice support has its downsides: sometimes it is complex to describe an issue in the app, and it requires the user’s full attention on the call.</p><p>With chat messaging apps growing massively in the last years, chat has become the expected support channel users are familiar with. It offers real-time support with the option of multitasking and easily explaining the issue by sharing pictures and documents. Compared to voice support, chat provides access to the conversation for future reference.</p><p>With chat growth, building a chat system tailored to our support needs and integrated with internal data, seemed to be the next natural move.</p><p>In our previous articles, we covered the tech challenges of <a href="https://engineering.grab.com/how-we-built-our-in-house-chat-platform-for-the-web">building the chat platform for the web</a>, our <a href="https://engineering.grab.com/customer-support-workforce-routing">workforce routing system</a> and <a href="https://engineering.grab.com/how-we-improved-agent-chat-efficiency-with-ml">improving agent efficiency with machine learning</a>. In this article, we will explain our approach and key learnings when building our in-house chat for support from a Product and Design angle.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/979/0*S4VxIt-ohGEhPXGH.gif" /><figcaption><em>A glimpse at agent and user experience</em></figcaption></figure><h3>Why Reinvent the Wheel</h3><p>We wanted to deliver a product that would fully delight our users. That’s why we decided to build an in-house chat tool that can:</p><ol><li><strong>Prevent chat disconnections and ensure a consistent chat experience</strong>: Building a native chat experience allowed us to ensure a stable chat session, even when users leave the app. Besides, leveraging on the existing Grab chat infrastructure helped us achieve this fast and ensure the chat experience is consistent throughout the app. You can read more about the chat architecture <a href="https://engineering.grab.com/how-we-built-our-in-house-chat-platform-for-the-web">here</a>.</li><li><strong>Improve productivity and provide faster support turnarounds</strong>: By building the agent experience in the CRM tool, we could reduce the number of tools the support team uses and build features tailored to our internal processes. This helped to provide faster help for our users.</li><li><strong>Allow integration with internal systems and services</strong>: Chat can be easily integrated with in-house AI models or chatbot, which helps us personalise the user experience and improve agent productivity.</li><li><strong>Route our users to the best support specialist available</strong>: Our newly built routing system accounts for all the use cases we were wishing for such as prioritising certain requests, better distribution of the chat load during peak hours, making changes at scale and ensuring each chat is routed to the best support specialist available.</li></ol><h3>Fail Fast with an MVP</h3><p>Before building a full-fledged solution, we needed to prove the concept, an MVP that would have the key features and yet, would not take too much effort if it fails. To kick start our experiment, we established the success criteria for our MVP; how do we measure its success or failure?</p><h3>Defining What Success Looks Like</h3><p>Any experiment requires a hypothesis — something you’re trying to prove or disprove and it should relate to your final product. To tailor the final product around the success criteria, we need to understand how success is measured in our situation. In our case, disconnections during chat support was one of the key challenges faced so our hypothesis was:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/863/0*15InRX_jlBZSqmeA.png" /></figure><h3>Starting with Design Sprint</h3><p>Our design sprint aimed to <strong>solutionise a series of problem statements, and generate a prototype to validate our hypothesis</strong>. To spark ideation, we run sketching exercises such as <a href="https://designsprintkit.withgoogle.com/methodology/phase3-sketch/crazy-8s">Crazy 8</a>, <a href="https://designsprintkit.withgoogle.com/methodology/phase3-sketch/solution-sketch">Solution sketch</a> and end off with sharing and voting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iMJf5pV7S9M-Qhn6.jpg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Lg_ppz-2xtIa5Xk6.jpg" /><figcaption><em>Some of the prototypes built during the Design sprint</em></figcaption></figure><h3>Defining MVP Scope to Run the Experiment</h3><p>To test our hypothesis quickly, we had to cut the scope by focusing on the basic functionality of allowing chat message exchanges with one agent.</p><p>Here is the main flow and a sneak peek of the design:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9LC4Vw3WbHG_70NF.jpg" /><figcaption><em>Accepting chats</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/0*TnL01Kd-nZtq-GMc.gif" /><figcaption><em>Handling concurrent chats</em></figcaption></figure><h3>What We Learnt from the Experiment</h3><p>During the experiment, we had to constantly put ourselves in our users’ shoes as ‘we are not our users’. We decided to shadow our chat support agents and get a sense of the potential issues our users actually face. By doing so, we learnt a lot about how the tool was used and spotted several problems to address in the next iterations.</p><p>In the end, <strong>the experiment confirmed our hypothesis that having a native in-app chat was more stable than the previous chat in use</strong>, resulting in a better user experience overall.</p><h3>Starting with the End in Mind</h3><p>Once the experiment was successful, we focused on scaling. We defined the most critical jobs to be done for our users so that we could scale the product further. When designing solutions to tackle each of them, we ensured that the product would be flexible enough to address future pain points. Would this work for more channels, more users, more products, more countries?</p><p>Before scaling, the problems to solve were:</p><ul><li><strong>Monitoring the performance of the system in real-time</strong>, so that swift operational changes can be made to ensure users receive fast support;</li><li><strong>Routing each chat to the best agent available</strong>, considering skills, occupancy, as well as issue prioritisation. You can read more about the our routing system design <a href="https://engineering.grab.com/customer-support-workforce-routing">here</a>;</li><li><strong>Easily communicate with users and show empathy</strong>, for which we built file-sharing capabilities for both users and agents, as well as allowing emojis, which create a more personalised experience.</li></ul><h3>Scaling Efficiently</h3><p>We broke down the chat support journey to determine what areas could be improved.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*szgQj48mmDcmJeN1.png" /></figure><h3>Reducing Waiting Time</h3><p>When analysing the current wait time, we realised that when there was a surge in support requests, the average waiting time increased drastically. In these cases, most users would be unresponsive by the time an agent finally attends to them.</p><p>To solve this problem, the team worked on a dynamic queue limit concept based on <a href="https://en.wikipedia.org/wiki/Little%2527s_law">Little’s law</a>. The idea is that considering the number of incoming chats and the agents’ capacity, we can forecast the number of users we can handle in a reasonable time, and prevent the remaining from initiating a chat. When this happens, we ensure there’s a backup channel for support so that no user is left unattended.</p><p>This allowed us to <strong>reduce chat waiting time by ~30% and reduce unresponsive users by ~7%</strong>.</p><h3>Reducing Time to Reply</h3><p>A big part of the chat time is spent typing the message to send to the user. Although the previous tool had templated messages, we observed that 85% of them were free-typed. This is because agents felt the templates were impersonal and wanted to add their personal style to the messages.</p><p>With this information in mind, we knew we could help by providing autocomplete suggestions while the agents are typing. We built a machine learning based feature that considers several factors such as user type, the entry point to support, and the last messages exchanged, to suggest how the agent should complete the sentence. When this feature was first launched, we <strong>reduced the average chat time by 12%</strong>!</p><p>Read <a href="https://engineering.grab.com/how-we-improved-agent-chat-efficiency-with-ml">this</a> to find out more about how we built this machine learning feature, from defining the problem space to its implementation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/462/0*aLYEzoT25yX-lvlT.gif" /></figure><h3>Reducing the Overall Chat Time</h3><p>Looking at the average chat time, we realised that there was still room for improvement. How can we help our agents to manage their time better so that we can reduce the waiting time for users in the queue?</p><p>We needed to provide visibility of chat durations so that our agents could manage their time better. So, we added a timer at the top of each chat window to indicate how long the chat was taking.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/855/0*kHS8XL7EUkWd203H.png" /><figcaption><em>Timer in the minimised chat</em></figcaption></figure><p>We also added nudges to remind agents that they had other users to attend to while they were in the chat.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/855/0*83m6b3l409qIVlpb.png" /><figcaption><em>Timer in the maximised chat</em></figcaption></figure><p>By providing visibility via prompts and colour-coded indicators to prevent exceeding the expected chat duration, we <strong>reduced the average chat time by 22%</strong>!</p><h3>What We Learnt from this Project</h3><ul><li><strong>Start with the end in mind.</strong> When you embark on a big project like this, have a clear vision of how the end state looks like and plan each step backwards. How does success look like and how are we going to measure it? How do we get there?</li><li><strong>Data is king.</strong> Data helped us spot issues in real-time and guided us through all the iterations following the MVP. It helped us prioritise the most impactful problems and take the right design decisions. Instrumentation must be part of your MVP scope!</li><li><strong>Remote user testing is better than no user testing at all.</strong> Ideally, you want to do user testing in the exact environment your users will be using the tool but a pandemic might make things a bit more complex. Don’t let this stop you! The qualitative feedback we received from real users, even with a prototype on a video call, helped us optimise the tool for their needs.</li><li><strong>Address the root cause, not the symptoms.</strong> Whenever you are tasked with solving a big problem, break it down into its components by asking “Why?” until you find the root cause. In the first phases, we realised the tool had a longer chat time compared to 3rd party softwares. By iteratively splitting the problem into smaller ones, we were able to address the root causes instead of the symptoms.</li><li><strong>Shadow your users whenever you can.</strong> By looking at the users in action, we learned a ton about their creative ways to go around the tool’s limitations. This allowed us to iterate further on the design and help them be more efficient.</li></ul><p><em>Of course, this would not have been possible without the incredible work of several teams: CSE, CE, Comms platform, Driver and Merchant teams.</em></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5ad2fc50f366" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/reshaping-chat-support-for-our-users-5ad2fc50f366">Reshaping Chat Support for Our Users</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Debugging High Latency Due to Context Leaks]]></title>
            <link>https://medium.com/grab/debugging-high-latency-due-to-context-leaks-2a364675a394?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/2a364675a394</guid>
            <category><![CDATA[memory-leak]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[latency]]></category>
            <category><![CDATA[debugging]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Wed, 14 Jul 2021 04:00:59 GMT</pubDate>
            <atom:updated>2021-07-14T04:05:04.994Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Sourabh Suman, Adarsh Koyya, ChandanKumar Agarwal</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*RB0Lo-1liEo29X3uVRW1Sw.jpeg" /></figure><h3>Background</h3><p>Market-Store is an in-house developed general purpose feature store that is used to serve real-time computed machine learning (ML) features. Market-Store has a stringent SLA around latency, throughput, and availability as it empowers ML models, which are used in Dynamic Pricing and Consumer Experience.</p><h3>Problem</h3><p>As Grab continues to grow, introducing new ML models and handling increased traffic, Market-Store started to experience high latency. Market-Store’s SLA states that 99% of transactions should be within 200ms, but our latency increased to 2 seconds. This affected the availability and accuracy of our models that rely on Market-Store for real-time features.</p><h3>Latency Issue</h3><p>We used different metrics and logs to debug the latency issue but could not find any abnormalities that directly correlated to the API’s performance. We discovered that the problem went away temporarily when we restarted the service. But during the next peak period, the service began to struggle once again and the problem became more prominent as Market-Store’s <a href="https://en.wikipedia.org/wiki/Queries_per_second">query per second (QPS)</a> increased.</p><p>The following graph shows an increase in the memory used with time over 12 hours. Even as the system load receded, memory usage continued to increase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wAt3riawzRUWq6PE.png" /></figure><p>The continuous increase in memory consumption indicated the possibility of a memory leak, which occurs when memory is allocated but not returned after its use is over. This results in consistently increasing consumed memory until the service runs out of memory and crashes.</p><p>Although we could restart the service and resolve the issue temporarily, the increasing memory use suggested a deeper underlying root cause. This meant that we needed to conduct further investigation with tools that could provide deeper insights into the memory allocations.</p><h3>Debugging Using Go Tools</h3><p><a href="https://golang.org/pkg/net/http/pprof/">PPROF</a> is a profiling tool by Golang that helps to visualise and analyse profiles from Go programmes. A profile is a collection of stack traces showing the call sequences in your programme that eventually led to instances of a particular event i.e. allocation. It also provides details such as Heap and CPU information, which could provide insights into the bottlenecks of the Go programme.</p><p>By default, PPROF is enabled on all Grab Go services, making it the ideal tool to use in our scenario. To understand how memory is allocated, we used PPROF to generate Market-Store’s Heap profile, which can be used to understand how inuse memory was allocated for the programme.</p><p>You can collect the Heap profile by running this command:</p><pre>go tool pprof &#39;<a href="http://localhost:6060/debug/pprof/heap">http://localhost:6060/debug/pprof/heap</a>&#39;</pre><p>The command then generates the Heap profile information as shown in the diagram below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/0*2mRTG6ey5GurBwOU.png" /></figure><p>From this diagram, we noticed that a lot of memory was allocated and held by the child context created from Async Library even after the tasks were completed.</p><p>In Market-Store, we used the <a href="https://github.com/grab/async">Async Library</a>, a Grab open-source library, which typically used to run concurrent tasks. Any contexts created by the Async Library should be cleaned up after the background tasks are completed. This way, memory would be returned to the service.</p><p>However, as shown in the diagram, memory was not being returned, resulting in a memory leak, which explains the increasing memory usage even as Market-Store’s system load decreased.</p><h3>Uncovering the Real Issue</h3><p>So we knew that Market-Store’s latency was affected, but we didn’t know why. From the first graph, we saw that memory usage continued to grow even as Market-Store’s system load decreased. Then, PPROF showed us that the memory held by contexts was not cleaned up, resulting in a memory leak.</p><p>Through our investigations, we drew a correlation between the increase in memory usage and a degradation in the server’s API latency. In other words, the memory leak resulted in a high memory consumption and eventually, caused the latency issue.</p><p>However, there was no change in our service that would have impacted how contexts are created and cleaned up. So what caused the memory leak?</p><h3>Debugging the Memory Leak</h3><p>We needed to look into the Async Library and how it worked. For Market-Store, we updated the cache asynchronously for the write-around caching mechanism. We use the Async Library for running the update tasks in the background.</p><p>The following code snippet explains how the Async Library works:</p><pre>async<strong>.</strong>Consume(context<strong>.</strong>Background(), runtime<strong>.</strong>NumCPU()<strong>*</strong>4, buffer)</pre><pre><em>// Consume runs the tasks with a specific max concurrency</em></pre><pre><strong>func</strong> Consume(ctx context<strong>.</strong>Context, concurrency <strong>int</strong>, tasks <strong>chan</strong> Task) Task {</pre><pre>   <em>// code...</em></pre><pre>   <strong>return</strong> Invoke(ctx, <strong>func</strong>(context<strong>.</strong>Context) (<strong>interface</strong>{}, <strong>error</strong>) {</pre><pre>       workers <strong>:=</strong> make(<strong>chan</strong> <strong>int</strong>, concurrency)</pre><pre>       concurrentTasks <strong>:=</strong> make([]Task, concurrency)</pre><pre>       <em>// code ...</em></pre><pre>       t<strong>.</strong>Run(ctx)<strong>.</strong>ContinueWith(ctx, <strong>func</strong>(<strong>interface</strong>{}, <strong>error</strong>) (<strong>interface</strong>{}, <strong>error</strong>) {</pre><pre>       <em>// code...</em></pre><pre>      })</pre><pre>    }</pre><pre>}</pre><pre><strong>func</strong> Invoke(ctx context<strong>.</strong>Context, action Work) Task {</pre><pre>    <strong>return</strong> NewTask(action)<strong>.</strong>Run(ctx)</pre><pre>}</pre><pre><strong>func</strong>(t <strong>*</strong>task) Run(ctx context<strong>.</strong>Context) Task {</pre><pre>    ctx, t<strong>.</strong>cancel <strong>=</strong> context<strong>.</strong>WithCancel(ctx)</pre><pre>    <strong>go</strong> t<strong>.</strong>run(ctx)</pre><pre>    <strong>return</strong> t</pre><pre>}</pre><p><em>Note: Code that is not relevant to this article was replaced with </em><em>code.</em></p><p>As seen in the code snippet above, the Async Library initialises the Consume method with a background context, which is then passed to all its runners. Background contexts are empty and do not track or have links to child contexts that are created from them.</p><p>In Market-Store, we use background contexts because they are not bound by request contexts and can continue running even after a request context is cleaned up. This means that once the task has finished running, the memory consumed by child contexts would be freed up, avoiding the issue of memory leaks altogether.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*7hhxVK_On7hNmolx.gif" /></figure><h3>Identifying the Cause of the Memory Leak</h3><p>Upon further digging, we discovered an <a href="https://github.com/grab/async/commit/d7b10a27c97049564607012efaceb28ccd32e980">MR</a> that was merged into the library to address a task cancellation issue. As shown in the code snippet below, the Consume method had been modified such that task contexts were being passed to the runners, instead of the empty background contexts.</p><pre><strong>func</strong> Consume(ctx context<strong>.</strong>Context, concurrency <strong>int</strong>, tasks <strong>chan</strong> Task) Task {</pre><pre>     <em>// code...</em></pre><pre>     <strong>return</strong> Invoke(ctx, <strong>func</strong>(taskCtx context<strong>.</strong>Context) (<strong>interface</strong>{}, <strong>error</strong>) {</pre><pre>         workers <strong>:=</strong> make(<strong>chan</strong> <strong>int</strong>, concurrency)</pre><pre>         concurrentTasks <strong>:=</strong> make([]Task, concurrency)</pre><pre>         <em>// code ...</em></pre><pre>         t<strong>.</strong>Run(taskCtx)<strong>.</strong>ContinueWith(ctx, <strong>func</strong>(<strong>interface</strong>{}, <strong>error</strong>) (<strong>interface</strong>{}, <strong>error</strong>) {</pre><pre>            <em>// code...</em></pre><pre>        })</pre><pre>     }</pre><pre>}</pre><p>Before we explain the code snippet, we should briefly explain what Golang contexts are. A <a href="https://golang.org/pkg/context/">context</a> is a standard Golang package that carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. We should always remember to cancel contexts after using them.</p><h4>Importance of Context Cancellation</h4><p>When a context is cancelled, all contexts derived from it are also cancelled. This means that there will be no unaccounted contexts or links and it can be achieved by using the Async Library’s CancelFunc.</p><p>The Async Library’s CancelFunc method will:</p><ul><li>Cancel the created child context and its children</li><li>Remove the parent reference from the child context</li><li>Stop any associated timers</li></ul><p>We should always make sure to call the CancelFunc method after using contexts, to ensure that contexts and memory are not leaked.</p><h4>Explaining the Impact of the MR</h4><p>In the previous code snippet, we see that task contexts are passed to runners and they are not being cancelled. The Async Library created task contexts from non-empty contexts, which means the task contexts are tracked by the parent contexts. So, even if the work associated with these task contexts is complete, they will not be cleaned up by the system (garbage collected).</p><p>As we started using task contexts instead of background contexts and did not cancel them, the memory used by these contexts was never returned, thus resulting in a memory leak.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*DaYXzx3l32wjCf4d.gif" /></figure><p>It took us several tries to debug and investigate the root cause of Market-Store’s high latency issue and through this incident, we learnt several important things that would help prevent a memory leak from recurring.</p><ul><li>Always cancel the contexts you’ve created. Leaving it to garbage collection (system cleanup) may result in unexpected memory leaks.</li><li>Go profiling can provide plenty of insights about your programme, especially when you’re not sure where to start troubleshooting.</li><li>Always benchmark your dependencies when integrating or updating the versions to ensure they don’t have any performance bottlenecks.</li></ul><p>Special thanks to <em>Chip Dong Lim</em> for his contributions and for designing the GIFs included in this article.</p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2a364675a394" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/debugging-high-latency-due-to-context-leaks-2a364675a394">Debugging High Latency Due to Context Leaks</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Hyper Self-Service, Distributed Tracing and Feedback System for Rule & Machine Learning…]]></title>
            <link>https://medium.com/grab/building-a-hyper-self-service-distributed-tracing-and-feedback-system-for-rule-machine-learning-2458ff58fff7?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/2458ff58fff7</guid>
            <category><![CDATA[statistics]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[distributed-tracing]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Wed, 14 Jul 2021 03:39:57 GMT</pubDate>
            <atom:updated>2021-07-14T03:39:57.531Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building a Hyper Self-Service, Distributed Tracing and Feedback System for Rule &amp; Machine Learning (ML) Predictions</h3><h4>By Warren Zhou, Wenhui Wu, Yongguo Mei, Muqi Li, Varun Kansal</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*nzhvS4LzmUrXPvgl7hdU0g.png" /></figure><h3>Introduction</h3><p>In Grab, the Trust, Identity, Safety, and Security (TISS) is a team of software engineers and AI developers working on fraud detection, login identity check, safety issues, etc. There are many TISS services, like grab-fraud, grab-safety, and grab-id. They make billions of business decisions daily using the <a href="https://engineering.grab.com/griffin">Griffin rule engine</a>, which determines if a passenger can book a trip, get a food promotion, or if a driver gets a delivery booking.</p><p>There is a natural demand to log down all these important business decisions, store them and query them interactively or in batches. Data analysts and scientists need to use the data to train their machine learning models. RiskOps and customer service teams can query the historical data and help consumers.</p><p>That’s where Archivist comes in; it is a new tracing, statistics and feedback system for rule and machine learning-based predictions. It is reliable and performant. Its innovative data schema is flexible for storing events from different business scenarios. Finally, it provides a user-friendly UI, which has access control for classified data.</p><p>Here are the impacts Archivist has already made:</p><ul><li>Currently, there are 2 teams with a total of 5 services and about 50 business scenarios using Archivist. The scenarios include fraud prevention (e.g. DriverBan, PassengerBan), payment checks (e.g. PayoutBlockCheck, PromoCheck), and identity check events like PinTrigger.</li><li>It takes only a few minutes to onboard a new business scenario (event type), by using the configuration page on the user portal. Previously, it took at least 1 to 2 days.</li><li>Each day, Archivist logs down 80 million logs to the ElasticSearch cluster, which is about 200GB of data.</li><li>Each week, Customer Experience (CE)/Risk Ops goes to the user portal and checks Archivist logs for about 2,000 distinct customers. They can search based on numerous dimensions such as the Passenger/DriverID, phone number, request ID, booking code and payment fingerprint.</li></ul><h3>Background</h3><p>Each day, TISS services make billions of business decisions (predictions), based on the Griffin rule engine and ML models.</p><p>After the predictions are made, there are still some tough questions for these services to answer.</p><ul><li>If Risk Ops believes a prediction is false-positive, a consumer could be banned. If this happens, how can consumers or Risk Ops report or feedback this information to the new rule and ML model training quickly?</li><li>As CustomService/Data Scientists investigating any tickets opened due to TISS predictions/decisions, how do you know which rules and data were used? E.g. why the passenger triggered a selfie, or why a booking was blocked.</li><li>After Data Analysts/Data Scientists (DA/DS) launch a new rule/model, how can they track the performance in fine-granularity and in real-time? E.g. week-over-week rule performance in a country or city.</li><li>How can DA/DS access all prediction data for data analysis or model training?</li><li>How can the system keep up with Grab’s business launch speed, with maximum self-service?</li></ul><h3>Problem</h3><p>To answer the questions above, TISS services previously used company-wide Kibana to log predictions. For example, a log looks like: PassengerID:123,Scenario:PinTrigger,Decision:Trigger,.... This logging method had some obvious issues:</p><ul><li>Logs in plain text don’t have any structure and are not friendly to ML model training as most ML models need processed data to make accurate predictions.</li><li>Furthermore, there is no fine-granularity access control for developers in Kibana.</li><li>Developers, DA and DS have no access control while CEs have no access at all. So CE cannot easily see the data and DA/DS cannot easily process the data.</li></ul><p>To address all the Kibana log issues, we developed ActionTrace, a code library with a well-structured data schema. The logs, also called documents, are stored in a dedicated ElasticSearch cluster with access control implemented. However, after using it for a while, we found that it still needed some improvements.</p><ol><li>Each business scenario involves different types of entities and ActionTrace is not fully self-service. This means that a lot of development work was needed to support fast-launching business scenarios. Here are some examples:</li></ol><ul><li>The main entities in the taxi business are Driver and Passenger,</li><li>The main entities in the food business can be Merchant, Driver and Consumer.</li></ul><ol><li>All these entities will need to be manually added into the ActionTrace data schema.</li><li>Each business scenario may have their own custom information logged. Because there is no overlap, each of them will correspond to a new field in the data schema. For example:</li></ol><ul><li>For any scenario involving payment, a valid payment method and expiration date is logged.</li><li>For the taxi business, the geohash is logged.</li></ul><ol><li>To store the log data from ActionTrace, different teams need to set up and manage their own ElasticSearch clusters. This increases hardware and maintenance costs.</li><li>There was a simple Web UI created for viewing logs from ActionTrace, but there was still no access control in fine granularity.</li></ol><h3>Solution</h3><p>We developed Archivist, a new tracing, statistics, and feedback system for ML/rule-based prediction events. It’s centralised, performant and flexible. It answers all the issues mentioned above, and it is an improvement over all the existing solutions we have mentioned previously.</p><p>The key improvements are:</p><ul><li>User-defined entities and custom fields</li><li>There are no predefined entity types. Users can define up to 5 entity types (E.g. PassengerId, DriverId, PhoneNumber, PaymentMethodId, etc.).</li><li>Similarly, there are a limited number of custom data fields to use, in addition to the common data fields shared by all business scenarios.</li><li>A dedicated service shared by all other services</li><li>Each service writes its prediction events to a Kafka stream. Archivist then reads the stream and writes to the ElasticSearch cluster.</li><li>The data writes are buffered, so it is easy to handle traffic surges in peak time.</li><li>Different services share the same Elastic Cloud Enterprise (ECE) cluster, but they create their own daily file indices so the costs can be split fairly.</li><li>Better support for data mining, prediction stats and feedback</li><li>Kafka stream data are simultaneously written to AWS S3. DA/DS can use the PrestoDB SQL query engine to mine the data.</li><li>There is an internal web portal for viewing Archivist logs. Customer service teams and Ops can use no-risk data to address CE tickets, while DA, DS and developers can view high-risk data for code/rule debugging.</li><li>A reduction of development days to support new business launches</li><li>Previously, it took a week to modify and deploy the ActionTrace data schema. Now, it only takes several minutes to configure event schemas in the user portal.</li><li>Saves time in RiskOps/CE investigations</li><li>With the new web UI which has access control in place, the different roles in the company, like Customer service and Data analysts, can access the Archivist events with different levels of permissions.</li><li>It takes only a few clicks for them to find the relevant events that impact the drivers/passengers.</li></ul><h3>Architecture Details</h3><p>Archivist’s system architecture is shown in the diagram below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g13b0NZG7UBND8UR.png" /><figcaption><em>Archivist system architecture</em></figcaption></figure><ul><li>Different services (like fraud-detection, safety-allocation, etc.) use a simple SDK to write data to a Kafka stream (the left side of the diagram).</li><li>In the centre of Archivist is an event processor. It reads data from Kafka, and writes them to ElasticSearch (ES).</li><li>The Kafka stream writes to the Amazon S3 data lake, so DA/DS can use the Presto SQL query engine to query them.</li><li>The user portal (bottom right) can be used to view the Archivist log and update configurations. It also sends all the web requests to the API Handler in the centre.</li></ul><p>The following diagram shows how internal and external users use Archivist as well as the interaction between the <a href="https://engineering.grab.com/griffin">Griffin rule engine</a> and Archivist.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YRuEbtX9xIcvmQNw.png" /><figcaption><em>Archivist use cases</em></figcaption></figure><h3>Flexible Event Schema</h3><p>In Archivist, a prediction/decision is called an event. The event schema can be divided into 3 main parts conceptually.</p><ol><li>Data partitioning: Fields like service_name and event_type categorise data by services and business scenarios.Field nameTypeExampleNotesservice_namestringGrabFraudName of the Serviceevent_typestringPreRidePaxBan/SafeAllocation</li><li>Business decision making: request_id, decisions, reasons, event_content are used to record the business decision, the reason and the context (E.g. The input features of machine learning algorithms).Field nameTypeExampleNotesrequest_idstringa16756e8-efe2-472b-b614-ec6ae08a5912a 32-digit id for web requestsevent_contentstringEvent contextdecisions[string][&quot;NotAllowBook&quot;, &quot;SMS&quot;]A listreasonsstringjson payload string of the response from engine.</li><li>Customisation: Archivist provides user-defined entities and custom fields that we feel are sufficient and flexible for handling different business scenarios.Field nameTypeExampleNotesentity_type_1stringPassengerentity_id_1string12151entity_type_2stringDriverentity_id_2string341521-rdxf36767…stringentity_id_5stringcustom_field_type_1string“MessageToUser”custom_field_1string”please contact Ops”User defined fieldscustom_field_type_2“Prediction rule:”custom_field_2string“ML rule: 123, version:2”…stringcustom_field_6string</li></ol><h3>A User Portal to Support Querying, Prediction Stats and Feedback</h3><p>DA, DS, Ops and CE can access the internal user portal to see the prediction events, individually and on an aggregated city level.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/0*-AGsCK6X9P6BvLV5.gif" /><figcaption><em>A snapshot of the Archivist logs showing the aggregation of the data in each city</em></figcaption></figure><p>There are graphs on the portal, showing the rule/model performance on individual customers over a period of time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/0*V0Uw9rcke_-dsJd3.gif" /><figcaption><em>Rule performance on a customer over a period of time</em></figcaption></figure><h3>How to Use Archivist for Your Service</h3><p>If you want to get onboard Archivist, the coding effort is minimal. Here is an example of a code snippet to log an event:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t3P4XCMSN3HOARhi.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lRGtLR8KZ1h82cec.png" /><figcaption>Code snippet to log an event</figcaption></figure><h3>Lessons</h3><p>During the implementation of Archivist, we learnt some things:</p><ul><li>A good system needs to support multi-tenants from the beginning. Originally, we thought we could use just one Kafka stream, and put all the documents from different teams into one ElasticSearch (ES) index. But after one team insisted on keeping their data separately from others, we created more Kafka streams and ES indexes. We realised that this way, it’s easier for us to manage data and share the cost fairly.</li><li>Shortly after we launched Archivist, there was an incident where the ES data writes were choked. Because each document write is a goroutine, the number of goroutines increased to 400k and the memory usage reached 100% within minutes. We added a patch (2 lines of code) to limit the maximum number of goroutines in our system. Since then, we haven’t had any more severe incidents in Archivist.</li></ul><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2458ff58fff7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/building-a-hyper-self-service-distributed-tracing-and-feedback-system-for-rule-machine-learning-2458ff58fff7">Building a Hyper Self-Service, Distributed Tracing and Feedback System for Rule &amp; Machine Learning…</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our Journey to Continuous Delivery at Grab (Part 2)]]></title>
            <link>https://medium.com/grab/in-the-first-part-of-this-blog-post-youve-read-about-the-improvements-made-to-our-build-and-e56c90b56497?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/e56c90b56497</guid>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[continuous-deployment]]></category>
            <category><![CDATA[multi-cloud]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[continuous-delivery]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Fri, 21 May 2021 09:45:45 GMT</pubDate>
            <atom:updated>2021-05-21T09:46:31.995Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Sylvain Bougerel</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/920/1*AwXIRAR0bVOmW8rRaS9cWw.png" /></figure><p><a href="https://engineering.grab.com/our-journey-to-continuous-delivery-at-grab">In the first part of this blog post</a>, you’ve read about the improvements made to our build and staging deployment process, and how plenty of manual tasks routinely taken by engineers have been automated with <em>Conveyor</em>: an in-house continuous delivery solution.</p><p>This new post begins with the introduction of the hermeticity principle for our deployments, and how it improves the confidence with promoting changes to production. Changes sent to production via Conveyor’s deployment pipelines are then described in detail.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/0*CpRfSr8MytISZo6W.png" /></figure><p>Finally, looking back at the engineering efficiency improvements around velocity and reliability over the last 2 years, we answer the big question — was the investment on a custom continuous delivery solution like Conveyor the right decision for Grab?</p><h3>Improving Confidence in our Production Deployments with Hermeticity</h3><p>The term <em>deployment hermeticity</em> is borrowed from build systems. A build system is called hermetic if builds always produce the same artefacts regardless of changes in the environment they run on. Similarly, we call our deployments hermetic if they always result in the same deployed artefacts regardless of the environment’s change or the number of times they are executed.</p><p>The behaviour of a service is rarely controlled by a single variable. The application that makes up your service is an important driver of its behaviour, but its configuration is an important contributor, for example. The behaviour for traditional microservices at Grab is dictated mainly by 3 versioned artefacts: application code, static and dynamic configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/0*Y0lu7VC3M_4jvrZI.png" /></figure><p>Conveyor has been integrated with the systems that operate changes in each of these parameters. By tracking all 3 parameters at every deployment, Conveyor can reproducibly deploy microservices with similar behaviour: its deployments are hermetic.</p><p>Building upon this property, Conveyor can ensure that all deployments made to production have been tested before with the same combination of parameters. This is valuable to us:</p><ul><li>An outcome of staging deployments for a specific set of parameters is a good predictor of outcomes in production deployments for the same set of parameters and thus it makes testing in staging more relevant.</li><li>Rollbacks are hermetic; we never rollback to a combination of parameters that has not been used previously.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/887/0*NJfD9WRgfYQv6QLm.png" /></figure><p>In the past, incidents had resulted from an application rollback not compatible with the current dynamic configuration version; this was aggravating since rollbacks are expected to be a safe recovery mechanism. The introduction of hermetic deployments has largely eliminated this category of problems.</p><p>Hermeticity is maintained by registering the deployment parameters as artefacts after each successfully completed pipeline. Users must then select one of the registered deployment metadata to promote to production.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/940/0*tthsz7uRC_peSKRl.png" /></figure><p>At this point, you might be wondering: why not use a single pipeline that includes both staging and production deployments? This was indeed how it started, with a single pipeline spanning multiple environments. However, engineers soon complained about it.</p><p>The most obvious reason for the complaint was that less than 20% of changes deployed in staging will make their way to production. This meant that engineers would have toil associated with each completed staging deployment since the pipeline must be manually cancelled rather than continued to production.</p><p>The other reason is that this multi-environment pipeline approach reduced flexibility when promoting changes to production. There are different ways to apply changes to a cluster. For example, lengthy pipelines that refresh instances can be used to deploy any combination of changes, while there are quicker pipelines restricted to dynamic configuration changes (such as feature flags rollouts). Regardless of the order in which the changes are made and how they are applied, Conveyor tracks the change.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/0*2Ov5oZax6opJV1Yg.png" /></figure><p>Eventually, engineers promote a deployment artefact to production. However they do not need to apply changes in the same sequence with which were applied to staging. Furthermore, to prevent erroneous actions, Conveyor presents only changes that can be applied with the requested pipeline (and sometimes, no changes are available). Not being forced into a specific method of deploying changes is one of added benefits of hermetic deployments.</p><h3>Returning to Our Journey Towards Engineering Efficiency</h3><p>If you can recall, the first part of this blog post series ended with a description of staging deployment. Our deployment to production starts with a verification that we uphold our hermeticity principle, as explained above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/0*yfSwlHzSDEiVCvs3.png" /></figure><p>Our production deployment pipelines can run for several hours for large clusters with rolling releases (few run for days), so we start by acquiring locks to ensure there are no concurrent deployments for any given cluster.</p><p>Before making any changes to the environment, we automatically generate release notes, giving engineers a chance to abort if the wrong set of changes are sent to production.</p><p>The pipeline next waits for a deployment slot. Early on, engineers adopted deployment windows that coincide with office hours, such that if anything goes wrong, there is always someone on hand to help. Prior to the introduction of Conveyor, however, engineers would manually ask a Slack bot for approval. This interaction is now automated, and the only remaining action left is for the engineer to approve that the deployment can proceed via a single click, in line with our hands-off deployment principle.</p><p>When the canary is in production, Conveyor automates monitoring it. This process is similar to the one already discussed <a href="https://engineering.grab.com/our-journey-to-continuous-delivery-at-grab">in the first part of this blog post</a>: Engineers can configure a set of alerts that Conveyor will keep track of. As soon as any one of the alerts is triggered, Conveyor automatically rolls back the service.</p><p>If no alert is raised for the duration of the monitoring period, Conveyor waits again for a deployment slot. It then publishes the release notes for that deployment and completes the deployments for the cluster. After the lock is released and the deployment registered, the pipeline finally comes to its successful completion.</p><h3>Benefits of Our Journey Towards Engineering Efficiency</h3><p>All these improvements made over the last 2 years have reduced the effort spent by engineers on deployment while also reducing the failure rate of our deployments.</p><p>If you are an engineer working on DevOps in your organisation, you know how hard it can be to measure the impact you made on your organisation. To estimate the time saved by our pipelines, we can model the activities that were previously done manually with a rudimentary weighted graph. In this graph, each edge carries a probability of the activity being performed (100% when unspecified), while each vertex carries the time taken for that activity.</p><p>Focusing on our regular staging deployments only, such a graph would look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/687/0*FIgax-EkhXTnRk6V.png" /></figure><p>The overall amount of effort automated by the staging pipelines (<em>Estg</em>) is represented in the graph above. It can be converted into the equation below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/333/0*Dw8fzE3kZw6CVZSP.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/292/0*7KB7aCBZ5D_gd9E4.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/108/0*fFTvnHhbcI2WWTWX.png" /></figure><p>This equation shows that for each staging deployment, around 16 minutes of work have been saved. Similarly, for regular production deployments, we find that 67 minutes of work were saved for each deployment:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/658/0*kFK-K-02DuDgFsKL.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/399/0*dUx-FAf1QT3wZMtF.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/113/0*l6oPsHEHUOnXpF4y.png" /></figure><p>Moreover, efficiency was not the only benefit brought by the use of deployment pipelines for our traditional microservices. Surprisingly perhaps, the rate of failures related to production changes is progressively reducing while the amount of production changes that were made with Conveyor increased across the organisation (starting at 1.5% of failures per deployments, and finishing at 0.3% on average over the last 3 months for the period of data collected):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UYivDW2ljTaYnnKC.png" /></figure><h3>Keep Calm and Automate</h3><p>Since the first draft for this post was written, we’ve made many more improvements to our pipelines. We’ve begun automating Database Migrations; we’ve extended our set of hermetic variables to Amazon Machine Image (AMI) updates; and we’re working towards supporting container deployments.</p><p>Through automation, all of Conveyor’s deployment pipelines have contributed to save more than 5,000 man-days of efforts in 2020 alone, across all supported teams. That’s around 20 man-years worth of effort, which is around 3 times the capacity of the team working on the project! Investments in our automation pipelines have more than paid for themselves, and the gains go up every year as more workflows are automated and more teams are onboarded.</p><p>If Conveyor has saved efforts for engineering teams, has it then helped to improve velocity? I had opened the first part of this blog post with figures on the deployment funnel for microservice teams at Grab, towards the end of 2018. So where do the figures stand today for these teams?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/951/0*LHerQ5FpyZToue7d.png" /></figure><p>In the span of 2 years, the average number of build and staging deployment performed each day has not varied much. However, in the last 3 months of 2020, engineers have sent twice more changes to production than they did for the same period in 2018.</p><p>Perhaps the biggest recognition received by the team working on the project, was from Grab’s engineers themselves. In the 2020 internal NPS survey for engineering experience at Grab, Conveyor received the highest score of any tools (built in-house or not).</p><p><em>All these improvements in efficiency for our engineers would never have been possible without the hard work of all team members involved in the project, past and present: Tanun Chalermsinsuwan, Aufar Gilbran, Deepak Ramakrishnaiah, Repon Kumar Roy (Kowshik), Su Han, Voislav Dimitrijevikj, Stanley Goh, Htet Aung Shine, Evan Sebastian, Qijia Wang, Oscar Ng, Jacob Sunny, Subhodip Mandal and many others who have contributed and collaborated with them.</em></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e56c90b56497" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/in-the-first-part-of-this-blog-post-youve-read-about-the-improvements-made-to-our-build-and-e56c90b56497">Our Journey to Continuous Delivery at Grab (Part 2)</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Improved Agent Chat Efficiency with Machine Learning]]></title>
            <link>https://medium.com/grab/how-we-improved-agent-chat-efficiency-with-machine-learning-35c76e0063d7?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/35c76e0063d7</guid>
            <category><![CDATA[chat]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[consumer-support]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Wed, 21 Apr 2021 07:17:43 GMT</pubDate>
            <atom:updated>2021-04-21T07:17:43.044Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Suman Anand, Elisa Monacchi, Darrell Tay, Yun Zou, Wan Ling Guai</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*_UzcdyuoWAOS30KWuf9vAA.jpeg" /></figure><p>In previous articles (see <a href="https://engineering.grab.com/how-we-built-our-in-house-chat-platform-for-the-web">Grab’s in-house chat platform</a>, <a href="https://engineering.grab.com/customer-support-workforce-routing">workforce routing</a>), we shared how chat has grown to become one of the primary channels for support in the last few years.</p><p>With continuous chat growth and a new in-house tool, helping our agents be more efficient and productive was key to ensure a faster support time for our users and scale chat even further.</p><p>Starting from the analysis on the usage of another third-party tool as well as some shadowing sessions, we realised that building a templated-based feature wouldn’t help. We needed to offer personalisation capabilities, as our consumer support specialists care about their writing style and tone, and using templates often feels robotic.</p><p>We decided to build a machine learning model, called <strong>SmartChat</strong>, which offers contextual suggestions by leveraging several sources of internal data, helping our chat specialists type much faster, and hence serving more consumers.</p><p>In this article, we are going to explain the process from problem discovery to design iterations, and share how the model was implemented from both a data science and software engineering perspective.</p><h3>How SmartChat Works</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/462/0*bP7HAZh8K6kkXOU2.gif" /></figure><h3>Diving Deeper into the Problem</h3><p>Agent productivity became a key part in the process of scaling chat as a channel for support.</p><p>After splitting chat time into all its components, we noted that agent typing time represented a big portion of the chat support journey, making it the perfect problem to tackle next.</p><p>After some analysis on the usage of the third-party chat tool, we found out that even with functionalities such as canned messages, <strong>85% of the messages were still free typed</strong>.</p><p>Hours of shadowing sessions also confirmed that the consumer support specialists liked to add their own flair. They would often use the template and adjust it to their style, which took more time than just writing it on the spot. With this in mind, it was obvious that templates wouldn’t be too helpful, unless they provided some degree of personalisation.</p><p>We needed something that reduces typing time and also:</p><ul><li><strong>Allows some degree of personalisation</strong>, so that answers don’t seem robotic and repeated.</li><li><strong>Works with multiple languages and nuances</strong>, considering Grab operates in 8 markets, even some of the English markets have some slight differences in commonly used words.</li><li><strong>It’s contextual to the problem</strong> and takes into account the user type, issue reported, and even the time of the day.</li><li><strong>Ideally doesn’t require any maintenance effort</strong>, such as having to keep templates updated whenever there’s a change in policies.</li></ul><p>Considering the constraints, <strong>this seemed to be the perfect candidate for a machine learning-based functionality</strong>, which predicts sentence completion by considering all the context about the user, issue and even the latest messages exchanged.</p><h3>Usability is Key</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/863/0*-EZOwyfxenCgXM1R.png" /></figure><p>To fulfil the hypothesis, there are a few design considerations:</p><ol><li>Minimising the learning curve for agents.</li><li>Avoiding visual clutter if recommendations are not relevant.</li></ol><p>To increase the probability of predicting an agent’s message, one of the design explorations is to allow agents to select the top 3 predictions (Design 1). To onboard agents, we designed a quick tip to activate SmartChat using keyboard shortcuts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-gX5Dth7zs8k9wdD.png" /></figure><p>By displaying the top 3 recommendations, we learnt that it slowed agents down as they started to read all options even if the recommendations were not helpful. Besides, by triggering this component upon every recommendable text, it became a distraction as they were forced to pause.</p><p>In our next design iteration, we decided to leverage and reuse the interaction of SmartChat from a familiar platform that agents are using — Gmail’s Smart Compose. As agents are familiar with Gmail, the learning curve for this feature would be less steep. For first time users, agents will see a “Press tab” tooltip, which will activate the text recommendation. The tooltip will disappear after 5 times of use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-bnPaABv_WdAeEss.png" /></figure><p>To relearn the shortcut, agents can hover over the recommended text.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*k9idGy-XnrV51gww.png" /></figure><h3>How We Track Progress</h3><p>Knowing that this feature would come in multiple iterations, we had to find ways to track how well we were doing progressively, so we decided to measure the different components of chat time.</p><p>We realised that the agent typing time is affected by:</p><ul><li><strong>Percentage of characters saved</strong>. This tells us that the model predicted correctly, and also saved time. This metric should increase as the model improves.</li><li><strong>Model’s effectiveness</strong>. The agent writes the least number of characters possible before getting the right suggestion, which should decrease as the model learns.</li><li><strong>Acceptance rate</strong>. This tells us how many messages were written with the help of the model. It is a good proxy for feature usage and model capabilities.</li><li><strong>Latency</strong>. If the suggestion is not shown in about 100–200ms, the agent would not notice the text and keep typing.</li></ul><h3>Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/520/0*HqGQuqV1vGuT-Bg9.png" /></figure><p>The architecture involves support specialists initiating the fetch suggestion request, which is sent for evaluation to the machine learning model through API gateway. This ensures that only authenticated requests are allowed to go through and also ensures that we have proper rate limiting applied.</p><p>We have an internal platform called <strong>Catwalk</strong>, which is a microservice that offers the capability to execute machine learning models as a HTTP service. We used the Presto query engine to calculate and analyse the results from the experiment.</p><h3>Designing the Machine Learning Model</h3><p>I am sure all of us can remember an experiment we did in school when we had to catch a falling ruler. For those who have not done this experiment, feel free to try <a href="https://www.youtube.com/watch?v%3DLheOjO2DJD0">it</a> at home! The purpose of this experiment is to define a ballpark number for typical human reaction time (equations also included in the video link).</p><p>Typically, the human reaction time ranges from 100ms to 300ms, with a median of about 250ms (read more <a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4374455/">here</a>). Hence, we decided to set the upper bound for SmartChat response time to be 200ms while deciding the approach. Otherwise, the experience would be affected as the agents would notice a delay in the suggestions. To achieve this, we had to manage the model’s complexity and ensure that it achieves the optimal time performance.</p><p>Taking into consideration network latencies, the machine learning model would need to churn out predictions in less than 100ms, in order for the entire product to achieve a maximum 200ms refresh rate.</p><p>As such, a few key components were considered:</p><p><em>Model Tokenisation</em></p><ul><li>Model input/output tokenisation needs to be implemented along with the model’s core logic so that it is done in one network request.</li><li>Model tokenisation needs to be lightweight and cheap to compute.</li></ul><p><em>Model Architecture</em></p><ul><li>This is a typical sequence-to-sequence (seq2seq) task so the model needs to be complex enough to account for the auto-regressive nature of seq2seq tasks.</li><li>We could not use pure attention-based models, which are usually state of the art for seq2seq tasks, as they are bulky and computationally expensive.</li></ul><p><em>Model Service</em></p><ul><li>The model serving platform should be executed on a low-level, highly performant framework.</li></ul><p>Our proposed solution considers the points listed above. We have chosen to develop in Tensorflow (TF), which is a well-supported framework for machine learning models and application building.</p><p>For Latin-based languages, we used a simple whitespace tokenizer, which is serialisable in the TF graph using the tensorflow-text package.</p><pre>import tensorflow_text as text</pre><pre>tokenizer = text.WhitespaceTokenizer()</pre><p>For the model architecture, we considered a few options but eventually settled for a simple recurrent neural network architecture (RNN), in an Encoder-Decoder structure:</p><p><em>Encoder</em></p><ul><li>Whitespace tokenisation</li><li>Single layered Bi-Directional RNN</li><li>Gated-Recurrent Unit (GRU) Cell</li></ul><p><em>Decoder</em></p><ul><li>Single layered Uni-Directional RNN</li><li>Gated-Recurrent Unit (GRU) Cell</li></ul><p><em>Optimisation</em></p><ul><li>Teacher-forcing in training, Greedy decoding in production</li><li>Trained with a cross-entropy loss function</li><li>Using ADAM (Kingma and Ba) optimiser</li></ul><p><strong>Features</strong></p><p>To provide context for the sentence completion tasks, we provided the following features as model inputs:</p><ul><li>Past conversations between the chat agent and the user</li><li>Time of the day</li><li>User type (Driver-partners, Consumers, etc.)</li><li>Entrypoint into the chat (e.g. an article on cancelling a food order)</li></ul><p>These features give the model the ability to generalise beyond a simple language model, with additional context on the nature of contact for support. Such experiences also provide a better user experience and a more customised user experience.</p><p>For example, the model is better aware of the nature of time in addressing “Good <strong>{Morning/Afternoon/Evening}</strong>” given the time of the day input, as well as being able to interpret meal times in the case of food orders. E.g. “We have contacted the driver, your <strong>{breakfast/lunch/dinner}</strong> will be arriving shortly”.</p><h3>Typeahead Solution for the User Interface</h3><p>With our goal to provide a seamless experience in showing suggestions to accepting them, we decided to implement a typeahead solution in the chat input area. This solution had to be implemented with the ReactJS library, as the internal web-app used by our support specialist for handling chats is built in React.</p><p>There were a few ways to achieve this:</p><ol><li>Modify the Document Object Model (DOM) using Javascript to show suggestions by positioning them over the input HTML tag based on the cursor position.</li><li>Use a content editable div and have the suggestion span render conditionally.</li></ol><p>After evaluating the complexity in both approaches, the second solution seemed to be the better choice, as it is more aligned with the React way of doing things: avoid DOM manipulations as much as possible.</p><p>However, when a suggestion is accepted we would still need to update the content editable div through DOM manipulation. It cannot be added to React’s state as it creates a laggy experience for the user to visualise what they type.</p><p>Here is a code snippet for the implementation:</p><pre>import React, { Component } from &#39;react&#39;;<br>import liveChatInstance from &#39;./live-chat&#39;;</pre><pre>export class ChatInput extends Component {<br> constructor(props) {<br>   super(props);<br>   this.state = {<br>     suggestion: &#39;&#39;,<br>   };<br> }</pre><pre> getCurrentInput = () =&gt; {<br>   const { roomID } = this.props;<br>   const inputDiv = document.getElementById(`input_content_${roomID}`);<br>   const suggestionSpan = document.getElementById(<br>     `suggestion_content_${roomID}`,<br>   );</pre><pre>   // put the check for extra safety in case suggestion span is accidentally cleared<br>   if (suggestionSpan) {<br>     const range = document.createRange();<br>     range.setStart(inputDiv, 0);<br>     range.setEndBefore(suggestionSpan);<br>     return range.toString(); // content before suggestion span in input div<br>   }<br>   return inputDiv.textContent;<br> };</pre><pre> handleKeyDown = async e =&gt; {<br>   const { roomID } = this.props;<br>   // tab or right arrow for accepting suggestion<br>   if (this.state.suggestion &amp;&amp; (e.keyCode === 9 || e.keyCode === 39)) {<br>     e.preventDefault();<br>     e.stopPropagation();<br>     this.insertContent(this.state.suggestion);<br>     this.setState({ suggestion: &#39;&#39; });<br>   }<br>   const parsedValue = this.getCurrentInput();<br>   // space<br>   if (e.keyCode === 32 &amp;&amp; !this.state.suggestion &amp;&amp; parsedValue) {<br>     // fetch suggestion<br>     const prediction = await liveChatInstance.getSmartComposePrediction(<br>       parsedValue.trim(), roomID);<br>     this.setState({ suggestion: prediction })<br>   }<br> };</pre><pre> insertContent = content =&gt; {<br>   // insert content behind cursor<br>   const { roomID } = this.props;<br>   const inputDiv = document.getElementById(`input_content_${roomID}`);<br>   if (inputDiv) {<br>     inputDiv.focus();<br>     const sel = window.getSelection();<br>     const range = sel.getRangeAt(0);<br>     if (sel.getRangeAt &amp;&amp; sel.rangeCount) {<br>       range.insertNode(document.createTextNode(content));<br>       range.collapse();<br>     }<br>   }<br> };</pre><pre> render() {<br>   const { roomID } = this.props;<br>   return (<br>     &lt;div className=&quot;message_wrapper&quot;&gt;<br>       &lt;div<br>         id={`input_content_${roomID}`}<br>         role={&#39;textbox&#39;}<br>         contentEditable<br>         spellCheck<br>         onKeyDown={this.handleKeyDown}<br>       &gt;<br>         {!!this.state.suggestion.length &amp;&amp; (<br>           &lt;span<br>             contentEditable={false}<br>             id={`suggestion_content_${roomID}`}<br>           &gt;<br>             {this.state.suggestion}<br>           &lt;/span&gt;<br>         )}<br>       &lt;/div&gt;<br>     &lt;/div&gt;<br>   );<br> }<br>}</pre><p>The solution uses the spacebar as the trigger for fetching the suggestion from the ML model and stores them in a React state. The ML model prediction is then rendered in a dynamically rendered span.</p><p>We used the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection">window.getSelection()</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt">range</a> APIs to:</p><ul><li>Find the current input value</li><li>Insert the suggestion</li><li>Clear the input to type a new message</li></ul><p>The implementation has also considered the following:</p><ul><li><strong>Caching</strong>. API calls are made on every space character to fetch the prediction. To reduce the number of API calls, we also cached the prediction until it differs from the user input.</li><li><strong>Recover placeholder</strong>. There are data fields that are specific to the agent and consumer, such as agent name and user phone number, and these data fields are replaced by placeholders for model training. The implementation recovers the placeholders in the prediction before showing it on the UI.</li><li><strong>Control rollout</strong>. Since rollout is by percentage per country, the implementation has to ensure that only certain users can access predictions from their country chat model.</li><li><strong>Aggregate and send metrics</strong>. Metrics are gathered and sent for each chat message.</li></ul><h3>Results</h3><p>The initial experiment results suggested that we managed to save 20% of characters, which improved the <strong>efficiency of our agents by 12%</strong> as they were able to resolve the queries faster. These numbers exceeded our expectations and as a result, we decided to move forward by rolling SmartChat out regionally.</p><h3>What’s Next?</h3><p>In the upcoming iteration, we are going to focus on <strong>non-Latin language support</strong>, <strong>caching</strong>, and <strong>continuous training</strong>.</p><p><em>Non-Latin Language Support and Caching</em></p><p>The current model only works with Latin languages, where sentences consist of space-separated words. We are looking to provide support for non-Latin languages such as Thai and Vietnamese. The result would also be cached in the frontend to reduce the number of API calls, providing the prediction faster for the agents.</p><p><em>Continuous Training</em></p><p>The current machine learning model is built with training data derived from historical chat data. In order to teach the model and improve the metrics mentioned in our goals, we will enhance the model by letting it learn from data gathered in day-to-day chat conversations. Along with this, we are going to train the model to give better responses by providing more context about the conversations.</p><p>Seeing how effective this solution has been for our chat agents, we would also like to expose this to the end consumers to help them express their concerns faster and improve their overall chat experience.</p><p><em>Special thanks to </em><a href="/service/mailto:matthew.yeow@grabtaxi.com"><em>Kok Keong Matthew Yeow</em></a><em>, who helped to build the architecture and implementation in a scalable way.</em></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35c76e0063d7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/how-we-improved-agent-chat-efficiency-with-machine-learning-35c76e0063d7">How We Improved Agent Chat Efficiency with Machine Learning</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Optimally Scaling Kafka Consumer Applications]]></title>
            <link>https://medium.com/grab/optimally-scaling-kafka-consumer-applications-16d8ca300470?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/16d8ca300470</guid>
            <category><![CDATA[go]]></category>
            <category><![CDATA[event-sourcing]]></category>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[stream-processing]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Tue, 13 Apr 2021 09:39:08 GMT</pubDate>
            <atom:updated>2021-04-21T08:11:10.535Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Shubham Badkur</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*9g0D_wXwh5mkS-EyYqMK-w.png" /></figure><p>Earlier this year, we took you on a journey on how we built and deployed our event sourcing and stream processing framework at Grab. We’re happy to share that we’re able to reliably maintain our uptime and continue to service close to 400 billion events a week. We haven’t stopped there though. To ensure that we can scale our framework as the Grab business continuously grows, we have spent efforts optimising our infrastructure.</p><p>In this article, we will dive deeper into our Kubernetes infrastructure setup for our <a href="https://engineering.grab.com/plumbing-at-scale">stream processing framework</a>. We will cover why and how we focus on optimal scalability and availability of our infrastructure.</p><h3>Quick Architecture Recap</h3><figure><img alt="Coban Platform" src="https://cdn-images-1.medium.com/max/1024/1*j6TtRwzpOVmNwbvWTHL6cw.png" /></figure><p>The Coban platform provides lightweight <a href="https://medium.com/learning-the-go-programming-language/writing-modular-go-programs-with-plugins-ec46381ee1a9">Golang plugin</a> architecture-based data processing pipelines running in Kubernetes. These are essentially Kafka consumer pods that consume data, process it, and then materialise the results into various sinks (RDMS, other Kafka topics).</p><h3>Anatomy of a Processing Pod</h3><figure><img alt="Anatomy of a Stream Processing Pod" src="https://cdn-images-1.medium.com/max/1024/1*9-LF5JDi1pxhgZUw9whjrg.png" /></figure><p>Each stream processing pod (the smallest unit of a pipeline’s deployment) has three top level components:</p><ul><li><strong>Trigger</strong>: An interface that connects directly to the source of the data and converts it into an event channel.</li><li><strong>Runtime</strong>: This is the app’s entry point and the orchestrator of the pod. It manages the worker pools, triggers, event channels, and lifecycle events.</li><li><strong>Pipeline plugin</strong>: This is provided by the user, and conforms to a contract that the platform team publishes. It contains the domain logic for the pipeline and houses the pipeline orchestration defined by a user based on our Stream Processing Framework.</li></ul><h4>Optimal Scaling</h4><p>We initially architected our Kubernetes setup around <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale">horizontal pod autoscaling</a> (HPA), which scales the number of pods per deployment based on CPU and memory usage. HPA keeps CPU and memory per pod specified in the deployment manifest and scales horizontally as the load changes.</p><p>These were the areas of application wastage we observed on our platform:</p><ul><li>As Grab’s traffic is uneven, we’d always have to provision for peak traffic. As users would not (or could not) always account for ramps, they would be fairly liberal with setting limit values (CPU and memory), leading to resource wastage.</li><li>Pods often had uneven traffic distribution despite fairly even partition load distribution in Kafka. The Stream Processing Framework(SPF) is essentially Kafka consumers consuming from Kafka topics, hence the number of pods scaling in and out resulted in unequal partition load per pod.</li></ul><h4>Vertically Scaling with Fixed Number of Pods</h4><p>We initially kept the number of pods for a pipeline equal to the number of partitions in the topic the pipeline consumes from. This ensured even distribution of partitions to each pod providing balanced consumption. In order to abstract this from the end user, we automated the application deployment process to directly call the Kafka API to fetch the number of partitions during runtime.</p><p>After achieving a fixed number of pods for the pipeline, we wanted to move away from HPA. We wanted our pods to scale up and down as the load increases or decreases without any manual intervention. <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler">Vertical pod autoscaling</a> (VPA) solves this problem as it relieves us from any manual operation for setting up resources for our deployment.</p><p>We just deploy the application and let VPA handle the resources required for its operation. It’s known to not be very susceptible to quick load changes as it trains its model to monitor the deployment’s load trend over a period of time before recommending an optimal resource. This process ensures the optimal resource allocation for our pipelines considering the historic trends on throughput.</p><p>We saw a <em>~45%</em> reduction in our total resource usage vs resource requested after moving to VPA with a fixed number of pods from HPA.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B7ZBerHNe7DXR1CvdwuP_g.png" /></figure><h4>Managing Availability</h4><p>We broadly classify our workloads as latency sensitive (critical) and latency tolerant (non-critical). As a result, we could optimise scheduling and cost efficiency using <a href="https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption">priority classes</a> and <a href="https://github.com/kubernetes-sigs/cluster-proportional-autoscaler">overprovisioning</a> on heterogeneous node types on AWS.</p><h3>Kubernetes Priority Classes</h3><p>The main cost of running EKS in AWS is attributed to the EC2 machines that form the worker nodes for the Kubernetes cluster. Running <a href="https://aws.amazon.com/ec2/pricing/on-demand">On-Demand</a> brings all the guarantees of instance availability but it is definitely very expensive. Hence, our first action to drive cost optimisation was to include <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html">Spot instances</a> in our worker node group.</p><p>With the uncertainty of losing a spot instance, we started assigning priority to our various applications. We then let the user choose the priority of their pipeline depending on their use case. Different priorities would result in different node affinity to different kinds of instance groups (On-Demand/Spot). For example, Critical pipelines (latency sensitive) run on On-Demand worker node groups and Non-critical pipelines (latency tolerant) on Spot instance worker node groups.</p><p>We use priority class as a method of preemption, as well as a node affinity that chooses a certain priority pipeline for the node group to deploy to.</p><h3>Overprovisioning</h3><p>With spot instances running we realised a need to make our cluster quickly respond to failures. We wanted to achieve quick rescheduling of evicted pods, hence we added overprovisioning to our cluster. This means we keep some noop pods occupying free space running in our worker node groups for the quick scheduling of evicted or deploying pods.</p><p>The overprovisioned pods are the lowest priority pods, thus can be preempted by any pod waiting in the queue for scheduling. We used cluster proportional autoscaler to decide the right number of these overprovisioned pods, which scales up and down proportionally to cluster size (i.e number of nodes and CPU in worker node group). This relieves us from tuning the number of these noop pods as the cluster scales up or down over the period keeping the free space proportional to current cluster capacity.</p><p>Lastly, overprovisioning also helped improve the deployment time because there is no dependency on the time required for <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html">Auto Scaling Groups</a> (ASG) to add a new node to the cluster every time we want to deploy a new application.</p><h3>Future Improvements</h3><p>Evolution is an ongoing process. In the next few months, we plan to work on custom resources for combining VPA and fixed deployment size. Our current architecture setup works fine for now, but we would like to create a more tuneable in-house <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources">CRD(Custom Resource Definition)</a> for VPA that incorporates rightsizing our Kubernetes deployment horizontally.</p><p><em>Authored By Shubham Badkur on behalf of the Coban team at Grab — Ryan Ooi, Karan Kamath, Hui Yang, Yuguang Xiao, Jump Char, Jason Cusick, Shrinand Thakkar, Dean Barlan, Shivam Dixit, Andy Nguyen, and Ravi Tandon.</em></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><p><em>Originally published at </em><a href="https://engineering.grab.com/tackling-ui-test-execution-time-imbalance-for-xcode-parallel-testing"><em>https://engineering.grab.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=16d8ca300470" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/optimally-scaling-kafka-consumer-applications-16d8ca300470">Optimally Scaling Kafka Consumer Applications</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our Journey to Continuous Delivery]]></title>
            <link>https://medium.com/grab/our-journey-to-continuous-delivery-61bdf63fee35?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/61bdf63fee35</guid>
            <category><![CDATA[spinnaker]]></category>
            <category><![CDATA[continuous-delivery]]></category>
            <category><![CDATA[continuous-deployment]]></category>
            <category><![CDATA[cloud-agnostic]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Tue, 13 Apr 2021 09:30:46 GMT</pubDate>
            <atom:updated>2021-04-21T08:11:58.391Z</atom:updated>
            <content:encoded><![CDATA[<h4>By Sylvain Bougerel</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/920/1*-OEf3tyqrFLxDYahJFf1gw.png" /></figure><p>This blog post is a two-part presentation of the effort that went into improving the <a href="https://continuousdelivery.com/">continuous delivery</a> processes for backend services at Grab in the past two years. In the first part, we take stock of where we started two years ago and describe the software and tools we created while introducing some of the integrations we’ve done to automate our software delivery in our staging environment.</p><blockquote><strong><em>Continuous Delivery is the ability to get changes of all types — including new features, configuration changes, bug fixes and experiments — into production, or into the hands of users, safely and quickly in a sustainable way.</em><br><em> — </em></strong><a href="https://continuousdelivery.com/"><strong><em>continuousdelivery.com</em></strong></a></blockquote><p>As a backend engineer at Grab, nothing matters more than the ability to innovate quickly and safely. Around the end of 2018, Grab’s transportation and deliveries backend architecture consisted of roughly 270 services (the majority being microservices). The deployment process was lengthy, required careful inputs and clear communication. The care needed to push changes in production and the risk associated with manual operations led to the introduction of a Slack bot to coordinate deployments. The bot ensures that deployments occur only during off-peak and within work hours:</p><figure><img alt="Overview of the Grab Delivery Process" src="https://cdn-images-1.medium.com/max/928/1*pApZWlZb1wtPaiAu_ASauQ.png" /><figcaption><em>Overview of the Grab Delivery Process</em></figcaption></figure><p>Once the build was completed, engineers who desired to deploy their software to the Staging environment would copy release versions from the build logs, and paste them in a Jenkins job’s parameter. Tests needed to be manually triggered from another dedicated Jenkins job.</p><p>Prior to production deployments, engineers would generate their release notes via a script and update them manually in a wiki document. Deployments would be scheduled through interactions with a Slack bot that controls release notes and deployment windows. Production deployments were made once again by pasting the correct parameters into two dedicated Jenkins jobs, one for the canary (a.k.a. one-box) deployment and the other for the full deployment, spread one hour apart. During the monitoring phase, engineers would continuously observe metrics reported on our dashboards.</p><p>In spite of the fragmented process and risky manual operations impacting our velocity and stability, around 614 builds were running each business day and changes were deployed on our staging environment at an average rate of 300 new code releases per business day, while production changes averaged a rate of 28 new code releases per business day.</p><figure><img alt="Our Deployment Funnel, Towards the End of 2018" src="https://cdn-images-1.medium.com/max/801/1*CUPOdZXxyxbBq55psWgdjA.png" /><figcaption><em>Our Deployment Funnel, Towards the End of 2018</em></figcaption></figure><p>These figures meant that, on average, it took 10 business days between each service update in production, and only 10% of the staging deployments were eventually promoted to production.</p><h3>Automating Continuous Deployments at Grab</h3><p>With an increased focus on Engineering efficiency, in 2018 we started an internal initiative to address frictions in deployments that became known as Conveyor. To build Conveyor with a small team of engineers, we had to rely on an already mature platform which exhibited properties that are desirable to us to achieve our mission.</p><h4>Hands-off Deployments</h4><p>Deployments should be an afterthought. Engineers should be as removed from the process as possible, and whenever possible, decisions should be taken early, during the code review process. The machine will do the heavy lifting, and only when it can’t decide for itself, should the engineer be involved. Notifications can be leveraged to ensure that engineers are only informed when something goes wrong and a human decision is required.</p><figure><img alt="Hands-off Deployment Principle" src="https://cdn-images-1.medium.com/max/960/1*I8jrEPv79M9-PrDOSJOJDQ.png" /><figcaption><em>Hands-off Deployment Principle</em></figcaption></figure><h4>Confidence in Deployments</h4><p>Grab’s focus on gathering internal Engineering NPS feedback helped us collect valuable metrics. One of the metrics we cared about was our engineers’ confidence in their production deployments. A team’s entire deployment process to production could last for more than a day and may extend up to a week for teams with large infrastructures running critical services. The possibility of losing progress in deployments when individual steps may last for hours is detrimental to the improvement of Engineering efficiency in the organisation. The deployment automation platform is the bedrock of that confidence. If the platform itself fails regularly or does provide a path of upgrade that is transparent to end-users, any features built on top of it would suffer from these downtimes and ultimately erode confidence in deployments.</p><h4>Tailored to Most But Extensible for the Few</h4><p>Our backend engineering teams are working on diverse stacks, and so are their deployment processes. Right from the start, we wanted our product to benefit the largest population of engineers that had adopted the same process, so as to maximise returns on our investments. To ease adoption, we decided to tailor a deployment pipeline such that:</p><ol><li>It would model the exact sequence of manual processes followed by this population of engineers.</li><li>Switching to use that pipeline should require as little work as possible by service teams.</li></ol><p>However, in cases where this model would not fit a team’s specific process, our deployment platform should be open and extensible and support new customisations even when they are not originally supported by the product’s ecosystem.</p><h4>Cloud-agnosticity</h4><p>While we were going to target a specific process and team, to ensure that our solution would stand the test of time, we needed to ensure that our solution would support the variety of environments currently used in production. This variety was also likely to increase, and we wanted a platform that would mature together with the rest of our ecosystem.</p><h3>Overview of Conveyor</h3><h4>Setting Sail with Spinnaker</h4><p>Conveyor is based on <a href="https://spinnaker.io/&amp;usg=AOvVaw1a93_1MJmR_1SZQ0mlu4Ow">Spinnaker</a>, an open-source, multi-cloud continuous delivery platform. We’ve chosen Spinnaker over other platforms because it is a mature deployment platform with no single point of failure, supports complex workflows (referred to as pipelines in Spinnaker), and already supports a large array of cloud providers. Since Spinnaker is open-source and extensible, it allowed us to add the features we needed for the specificity of our ecosystem.</p><p>To further ease adoption within our organisation, we built a tailored user interface and created our own domain-specific language (DSL) to manage its pipelines as code.</p><figure><img alt="Outline of Conveyor’s Architecture" src="https://cdn-images-1.medium.com/max/922/1*D-ACnHHng9lUe4zjJZEk4A.png" /><figcaption><em>Outline of Conveyor’s Architecture</em></figcaption></figure><h4>Onboarding to a Simpler Interface</h4><p>Spinnaker comes with its own interface, it has all the features an engineer would want from an advanced continuous delivery system. However, Spinnaker interface is vastly different from Jenkins and makes for a steep learning curve.</p><p>To reduce our barrier to adoption, we decided early on to create a simple interface for our users. In this interface, deployment pipelines take the center stage of our application. Pipelines are objects managed by Spinnaker, they model the different steps in the workflow of each deployment. Each pipeline is made up of stages that can be assembled like lego-bricks to form the final pipeline. An instance of a pipeline is called an execution.</p><figure><img alt="Conveyor Dashboard" src="https://cdn-images-1.medium.com/max/1024/1*rad2UL8U4cjQY5ncb4MgBQ.png" /><figcaption><em>Conveyor Dashboard</em></figcaption></figure><p>With this interface, each engineer can focus on what matters to them immediately: the pipelines they have started, or those started by other teammates working on the same services as they are. Conveyor also provides a search bar (on the top) and filters (on the left) that work in concert to explore all pipelines executed at Grab.</p><p>We adopted a consistent set of colours to model all information in our interface:</p><ul><li>blue: represent stages that are currently running;</li><li>red: stages that have failed or important information;</li><li>yellow: stages that require human interaction;</li><li>and finally, in green: stages that were successfully completed.</li></ul><p>Conveyor also provides a task and notifications area, where all stages requiring human intervention are listed in one location. Manual interactions are often no more than just YES or NO questions:</p><figure><img alt="Conveyor Tasks" src="https://cdn-images-1.medium.com/max/1024/1*zFqIkxNLj_Q0NGCm-56mNA.png" /><figcaption><em>Conveyor Tasks</em></figcaption></figure><p>Finally, in addition to supporting automated deployments, we greatly simplified the start of manual deployments. Instead of being required to copy/paste information, each parameter can be selected on the interface from a set of predefined items, sorted chronologically, and presented with contextual information to help engineers in their decision.</p><p>Several parameters are required for our deployments and their values are selected from the UI to ensure correctness.</p><figure><img alt="Simplified Manual Deployments" src="https://cdn-images-1.medium.com/max/1024/1*uUc3NOa3qeH1BPZCAZY52w.png" /><figcaption><em>Simplified Manual Deployments</em></figcaption></figure><h4>Ease of Adoption with Our Pipeline-as-code DSL</h4><p>Ease of adoption for the team is not simply about the learning curve of the new tools. We needed to make it easy for teams to configure their services to deploy with Conveyor. Since we focused on automating tasks that were already performed manually, we needed only to configure the layer that would enable the integration.</p><p>We set on creating a pipeline-as-code implementation when none were widely being developed in the Spinnaker community. It’s interesting to see that two years on, this idea has grown in parallel in the community, with the birth of other <a href="https://docs.armory.io/docs/spinnaker/using-dinghy/">pipeline-as-code implementations</a>. Our pipeline-as-code is referred to as the Pipeline DSL, and its configuration is located inside each team’s repository. Artificer is the name of our Pipeline DSL interpreter and it runs with every change inside our monorepository:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/574/1*4vm1M2hLTSkNpDfUeY_Uiw.png" /><figcaption><em>Artificer: Our Pipeline DSL</em></figcaption></figure><p>Pipelines are being updated at every commit if necessary.</p><p>Creating a conveyor.jsonnet file within the service’s directory of our monorepository with the few lines below is all that’s required for Artificer to do its work and get the benefits of automation provided by Conveyor’s pipeline:</p><pre>local <strong>default</strong> <strong>=</strong> <strong>import</strong> &#39;default.libsonnet&#39;;<br>[<br>  {<br>     name: &quot;service-name&quot;,<br>     group: [<br>       &quot;group-name&quot;,<br>     ]<br>  }<br>]</pre><p><em>Sample minimal </em><em>conveyor.jsonnet configuration to onboard services.</em></p><p>In this file, engineers simply specify the name of their service and the group that a user should belong to, to have deployment rights for the service.</p><p>Once the build is completed, teams can log in to Conveyor and start manual deployments of their services with our pipelines. Three pipelines are provided by default: the integration pipeline used for tests and developments, the staging pipeline used for pre-production tests, and the production pipeline for production deployment.</p><p>Thanks to the simplicity of this minimal configuration file, we were able to generate these configuration files for all existing services of our monorepository. This resulted in the automatic onboarding of a large number of teams and was a major contributing factor to the adoption of Conveyor throughout our organisation.</p><h3>Our Journey to Engineering Efficiency (for Backend Services)</h3><p>The sections below relate some of the improvements in engineering efficiency we’ve delivered since Conveyor’s inception. They were not made precisely in this order but for readability, they have been mapped to each step of the software development lifecycle.</p><h4>Automate Deployments at Build Time</h4><figure><img alt="Continuous Integration Job" src="https://cdn-images-1.medium.com/max/763/1*tcTxZZPX4-Rx9xwAQRJl3Q.png" /><figcaption><em>Continuous Integration Job</em></figcaption></figure><p>Continuous delivery begins with a pushed code commit in our trunk-based development flow. Whenever a developer pushes changes onto their development branch or onto the trunk, a continuous integration job is triggered on Jenkins. The products of this job (binaries, docker images, etc) are all uploaded into our artefact repositories. We’ve made two additions to our continuous integration process.</p><p>The first modification happens at the step “Upload &amp; Register artefacts”. At this step, each artefact created is now registered in Conveyor with its associated metadata. When and if an engineer needs to trigger a deployment manually, Conveyor can display the list of versions to choose from, eliminating the need for error-prone manual inputs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/997/1*z84S8C8ySPM46ahi5sZDmQ.png" /><figcaption><em>Staging</em></figcaption></figure><p>Each selectable version shows contextual information: title, author, version and link to the code change where it originated. During registration, the commit time is also recorded and used to order entries chronologically in the interface. To ensure this integration is not a single point of failure for deployments, manual input is still available optionally.</p><p>The second modification implements one of the essential feature continuous delivery: your deployments should happen often, automatically. Engineers are now given the possibility to start automatic deployments once continuous integration has successfully completed, by simply modifying their project’s continuous integration settings:</p><pre>{<br>  &quot;AfterBuild&quot;: [<br>    {<br>      &quot;AutoDeploy&quot;: {<br>        &quot;OnDiff&quot;: <strong>false</strong>,<br>        &quot;OnLand&quot;: <strong>true</strong><br>      },<br>      &quot;TYPE&quot;: &quot;conveyor&quot;<br>    }<br>  ],<br>  <em>// other settings...</em><br>}</pre><p><em>Sample settings needed to trigger auto-deployments. </em><em>Diff refers to code review submissions, and </em><em>Land refers to merged code changes.</em></p><h4>Staging Pipeline</h4><p>Before deploying a new artefact to a service in production, changes are validated on the staging environment. During the staging deployment, we verify that canary (one-box) deployments and full deployments with automated smoke and functional tests suites.</p><figure><img alt="Staging Pipeline" src="https://cdn-images-1.medium.com/max/942/1*-CEWjeV0SiSZoT3gkv1y9A.png" /><figcaption><em>Staging Pipeline</em></figcaption></figure><p>We start by acquiring a deployment lock for this service and this environment. This prevents another deployment of the same service on the same environment to happen concurrently, other deployments will be waiting in a FIFO queue until the lock is released.</p><p>The stage <em>“Compute Changeset”</em> ensures that the deployment is not a rollback. It verifies that the new version deployed does not correspond to a rollback by comparing the ancestry of the commits provided during the artefact registration at build time: since we automate deployments after the build process has completed, cases of rollback may occur when two changes are created in quick succession and the latest build completes earlier than the older one.</p><p>After the stage <em>“Deploy Canary”</em> has completed, smoke test run. There are three kinds of tests executed at different stages of the pipeline: smoke, functional and security tests. Smoke tests directly reach the canary instance’s endpoint, by-passing load-balancers. If the smoke tests fail, the canary is immediately rolled back and this deployment is terminated.</p><p>All tests are generated from the same builds as the artefact being tested and their versions must match during testing. To ensure that the right version of the test run and distinguish between the different kind of tests to perform, we provide additional metadata that will be passed by Conveyor to the tests system, known internally as Gandalf:</p><pre>local <strong>default</strong> <strong>=</strong> <strong>import</strong> &#39;default.libsonnet&#39;;<br>[<br>  {<br>    name: &quot;service-name&quot;,<br>    group: [<br>      &quot;group-name&quot;,<br>    ],<br>    gandalf_smoke_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/smoke/tests&quot;<br>      }<br>    ],<br>    gandalf_functional_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/functional/tests&quot;<br>      }<br>    ],<br>    gandalf_security_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/security/tests&quot;<br>      }<br>    ]<br>  }<br>]</pre><p><em>Sample </em><em>conveyor.jsonnet configuration with integration tests added.</em></p><p>Additionally, in parallel to the execution of the smoke tests, the canary is also being monitored from the moment its deployment has completed and for a predetermined duration. We leverage our integration with Datadog to allow engineers to select the alerts to monitor. If an alert is triggered during the monitoring period, and while the tests are executed, the canary is again rolled back, and the pipeline is terminated. Engineers can specify the alerts by adding them to the conveyor.jsonnet configuration file together with the monitoring duration:</p><pre>local <strong>default</strong> <strong>=</strong> <strong>import</strong> &#39;default.libsonnet&#39;;<br>[<br>  {<br>    name: &quot;service-name&quot;,<br>    group: [<br>      &quot;group-name&quot;,<br>    ],<br>    gandalf_smoke_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/smoke/tests&quot;<br>      }<br>    ],<br>    gandalf_functional_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/functional/tests&quot;<br>      }<br>    ],<br>    gandalf_security_tests: [<br>      {<br>        path: &quot;repo.internal/path/to/my/security/tests&quot;<br>      }<br>    ],<br>    monitor: {<br>      stg: {<br>        duration_seconds: 300,<br>        alarms: [<br>          {<br>            type: &quot;datadog&quot;,<br>            alert_id: 12345678<br>          },<br>          {<br>            type: &quot;datadog&quot;,<br>            alert_id: 23456789<br>          }<br>        ]<br>      }<br>    }<br>  }<br>]</pre><p><em>Sample </em><em>conveyor.jsonnet configuration with alerts in staging added.</em></p><p>When the smoke tests and monitor pass and the deployment of new artefacts is completed, the pipeline execution triggers functional and security tests. Unlike smoke tests, functional &amp; security tests run only after that step, as they communicate with the cluster through load-balancers, impersonating other services.</p><p>Before releasing the lock, release notes are generated to inform engineers of the delta of changes between the version they just released and the one currently running in production. Once the lock is released, the stage <em>“Check Policies”</em> verifies that the parameters and variable of the deployment obeys a specific set of criteria, for example: if its service metadata is up-to-date in our service inventory, or if the base image used during deployment is sufficiently recent.</p><p>Here’s how the policy stage, the engine, and the providers interact with each other:</p><figure><img alt="Check Policy Stage" src="https://cdn-images-1.medium.com/max/870/1*ytJgyposwXMrCbL97lv9Xg.png" /><figcaption><em>Check Policy Stage</em></figcaption></figure><p>In Spinnaker, each event of a pipeline’s execution updates the pipeline’s state in the database. The current state of the pipeline can be fetched by its API as a single JSON document, describing all information related to its execution: including its parameters, the contextual information related to each stage or even the response from the various interfacing components. The role of our <em>“Policy Check”</em> stage is to query this JSON representation of the pipeline, to extract and transform the variables which are forwarded to our policy engine for validation. Our policy engine gathers judgements passed by different policy providers. If the validation by the policy engine fails, the deployment is not rolled back this time; however, promotion to production is not possible and the pipeline is immediately terminated.</p><p>The journey through staging deployment finally ends with the stage <em>“Register Deployment”</em>. This stage registers that a successful deployment was made in our staging environment as an artefact. Similarly to the policy check above, certain parameters of the deployment are picked up and consolidated into this document. We use this kind of artefact as proof for upcoming production deployment.</p><h4>Continuing Our Journey to Engineering Efficiency</h4><p>With the advancements made in continuous integration and deployment to staging, Conveyor has reduced the efforts needed by our engineers to just three clicks in its interface, when automated deployment is used. Even when the deployment is triggered manually, Conveyor gives the assurance that the parameters selected are valid and it does away with copy/pasting and human interactions across heterogeneous tools.</p><p>In the sequel to this blog post, we’ll dive into the improvements that we’ve made to our production deployments and introduce a crucial concept that led to the creation of our proof for successful staging deployment. Finally, we’ll cover the impact that Conveyor had on the continuous delivery of our backend services, by comparing our deployment velocity when we started two years ago versus where we are today.</p><p><em>All these improvements in efficiency for our engineers would never have been possible without the hard work of all team members involved in the project, past and present: Evan Sebastian, Tanun Chalermsinsuwan, Aufar Gilbran, Deepak Ramakrishnaiah, Repon Kumar Roy (Kowshik), Su Han, Voislav Dimitrijevikj, Qijia Wang, Oscar Ng, Jacob Sunny, Subhodip Mandal, and many others who have contributed and collaborated with them.</em></p><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><p><em>Originally published at </em><a href="https://engineering.grab.com/tackling-ui-test-execution-time-imbalance-for-xcode-parallel-testing"><em>https://engineering.grab.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=61bdf63fee35" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/our-journey-to-continuous-delivery-61bdf63fee35">Our Journey to Continuous Delivery</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Uncovering the truth behind Lua and Redis data consistency]]></title>
            <link>https://medium.com/grab/uncovering-the-truth-behind-lua-and-redis-data-consistency-77c76431d5d7?source=rss----583648db97e0---4</link>
            <guid isPermaLink="false">https://medium.com/p/77c76431d5d7</guid>
            <category><![CDATA[high-cpu]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[data-consistency]]></category>
            <category><![CDATA[lua-script]]></category>
            <dc:creator><![CDATA[Grab]]></dc:creator>
            <pubDate>Tue, 13 Apr 2021 09:28:38 GMT</pubDate>
            <atom:updated>2021-04-21T08:14:39.458Z</atom:updated>
            <content:encoded><![CDATA[<h3>Uncovering the Truth Behind Lua and Redis Data Consistency</h3><h4>By Allen Wang</h4><p>Our team at Grab uses Redis as one of our message queues. The Redis server is deployed in a master/replica setup. Quite recently, we have been noticing a spike in the CPU usage of the Redis replicas every time we deploy our service, even when the replicas are not in use and when there’s no read traffic to it. However, the issue is resolved once we reboot the replica.</p><p>Because a reboot of the replica fixes the issue every time, we thought that it might be due to some Elasticache replication issues and didn’t pursue further. However, a recent Redis failover brought this to our attention again. After the failover, the problematic replica becomes the new master and its CPU immediately goes to 100% with the read traffic, which essentially means the cluster is not functional after the failover. And this time we investigated the issue with new vigour. What we found in our investigation led us to deep dive into the details of Redis replication and its implementation of Hash.</p><p>Did you know that Redis master/replica can become inconsistent in certain scenarios?</p><p>Did you know the encoding of Hash objects on the master and the replica are different even if the writing operations are exactly the same and in the same order? Read on to find out why.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*a7Lyt2hEoYO8y_l0EFxawA.png" /></figure><h3>The Problem</h3><p>The following graph shows the CPU utilisation of the master vs. the replica immediately after our service is deployed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UHiu7Nee50N19tGA.png" /><figcaption><em>CPU Utilisation</em></figcaption></figure><p>From the graph, you can see the following CPU usage trends. Replica’s CPU usage:</p><ul><li>Increases immediately after our service is deployed.</li><li>Spikes higher than the master after a certain time.</li><li>Get’s back to normal after a reboot.</li></ul><h3>Cursory Investigation</h3><p>Because the spike occurs only when we deploy our service, we scrutinised all the scripts that were triggered immediately after the deployment. Lua monitor script was identified as a possible suspect. The script redistributes inactive service instances’ messages in the queue to active service instances so that messages can be processed by other healthy instances.</p><p>We ran a few experiments related to the Lua monitor script using the Redis <a href="https://redis.io/commands/monitor">monitor</a> command to compare the script’s behaviour on master and the replica. A side note, because this command causes performance degradation, use it with discretion. Coming back to the script, we were surprised to note that the monitor script behaves differently between the master and the replica:</p><ul><li>Redis executes the script separately on the master and the replica. We expected the script to execute only on master and the resulting changes to be replicated to the secondary.</li><li>The Redis command HGETALL used in the script returns the hash keys in a different order on master compared to the replica.</li></ul><p>Due to the above reasons, the script causes data inconsistencies between the master and its replica. From that point on, the data between the master and the replica keeps diverging till they become completely distinct. Due to the inconsistency, the data on the secondary does not get deleted correctly thereby growing into an extremely large dataset. Any further operations on the large dataset requires a higher CPU usage, which explains why the replica’s CPU usage is higher than the master.</p><p>During replica reboots, the data gets synced and consistent again, which is why the CPU usage gets to normal values after rebooting.</p><h3>Diving Deeper on HGETALL</h3><p>We knew that the keys of a hash are not ordered and we should not rely on the order. But it still puzzled us that the order is different even when the writing sequence is the same between the master and the replica. Plus the fact that the orders are always the same in our local environment with a similar setup made us even more curious.</p><p>So to better understand the underlying magic of Redis and to avoid similar bugs in the future, we decided to hammer on and read the Redis source code to get more details.</p><h3>HGETALL Command Handling Code</h3><p>The HGETALL command is handled by the function genericHgetallCommand and it further calls hashTypeNext to iterate through the Hash object. A snippet of the code is shown as follows:</p><pre>/* Move to the next entry in the hash. Return C_OK when the next entry<br> * could be found and C_ERR when the iterator reaches the end. */<br>int hashTypeNext(hashTypeIterator *hi) {<br>    if (hi-&gt;encoding == OBJ_ENCODING_ZIPLIST) {<br>        // call zipListNext<br>    } else if (hi-&gt;encoding == OBJ_ENCODING_HT) {<br>        // call dictNext<br>    } else {<br>        serverPanic(&quot;Unknown hash encoding&quot;);<br>    }<br>    return C_OK;<br>}</pre><p>From the code snippet, you can see that the Redis Hash object actually has two underlying representations:</p><ul><li>ZIPLIST</li><li>HASHTABLE (dict)</li></ul><p>A bit of research online helped us understand that, to save memory, Redis chooses between the two hash representations based on the following limits:</p><ul><li>By default, Redis stores the Hash object as a <a href="https://redis.io/topics/memory-optimization">zipped list</a> when the hash has less than 512 entries and when each element’s size is smaller than 64 bytes.</li><li>If either limit is exceeded, Redis converts the list to a <a href="https://github.com/antirez/redis/blob/3.2/src/t_hash.c#L40">hashtable</a>, and this is irreversible. That is, Redis won’t convert the hashtable back to a list again, even if the entries/size falls below the limit.</li></ul><h3>Eureka Moment</h3><p>Based on this understanding, we checked the encoding of the problematic hash in staging.</p><pre><a href="http://stg-bookings-qu-002.pcxebj.0001.apse1.cache.amazonaws.com/">stg-bookings-qu-002.pcxebj.0001.apse1.cache.amazonaws.com</a>:6379&gt; object encoding queue_stats<br>&quot;hashtable&quot;</pre><pre><a href="http://stg-bookings-qu-001.pcxebj.0001.apse1.cache.amazonaws.com/">stg-bookings-qu-001.pcxebj.0001.apse1.cache.amazonaws.com</a>:6379&gt; object encoding queue_stats<br>&quot;ziplist&quot;</pre><p><strong>To our surprise, the encodings of the Hash object on the master and its replica were different.</strong> Which means if we add or delete elements in the hash, the sequence of the keys won’t be the same due to hashtable operation vs. list operation!</p><p>Now that we have identified the root cause, we were still curious about the difference in encoding between the master and the replica.</p><h3>How Could the Underlying Representations be Different?</h3><p>We reasoned, “<em>If the master and its replica’s writing operations are exactly the same and in the same order, why are the underlying representations still different?</em>”</p><p>To answer this, we further looked through the Redis source to find all the possible places that a Hash object’s representation could be changed and soon found the following code snippet:</p><pre>/* Load a Redis object of the specified type from the specified file.<br> * On success a newly allocated object is returned, otherwise NULL. */<br>robj *rdbLoadObject(int rdbtype, rio *rdb) {<br>  //...<br>  if (rdbtype == RDB_TYPE_HASH) {<br>    //...<br>    o = createHashObject();  // ziplist</pre><pre>    /* Too many entries? Use a hash table. */<br>    if (len &gt; server.hash_max_ziplist_entries)<br>        hashTypeConvert(o, OBJ_ENCODING_HT);</pre><pre>    //...<br>  }<br>}</pre><p>Reading through the code we understand the following behaviour:</p><ul><li>When restoring from an RDB file, Redis creates a ziplist first for Hash objects.</li><li>Only when the size of the Hash object is greater than the hash_max_ziplist_entries, the ziplist is converted to a hashtable.</li></ul><p>So, if you have a Redis Hash object encoded as a hashtable with its length less than hash_max_ziplist_entries (512) in the master, when you set up a replica, it is encoded as a ziplist.</p><p>We were able to verify this behaviour in our local setup as well.</p><h3>How did We Fix it?</h3><p>We could use the following two approaches to address this issue:</p><ul><li>Enable<a href="https://redis.io/commands/eval#replicating-commands-instead-of-scripts"> script effect replication mode</a>. This tells Redis to replicate the commands generated by the script instead of running the whole script on the replica. One disadvantage to using this approach is that it adds network traffic between the master and the replica.</li><li>Ensure the behaviour of the Lua monitor script is deterministic. In our case, we can do this by sorting the outputs of HKEYS/HGETALL.</li></ul><p>We chose the latter approach because:</p><ul><li>The Hash object is pretty small ( &lt; 30 elements) so the sorting overhead is low, less than 1ms for 100 elements based on our tests.</li><li>Replicating our script effect would end up replicating thousands of Redis writing commands on the secondary causing a much higher overhead compared to replicating just the script.</li></ul><p>After the fix, the CPU usage of the replica remained in range after each deployment. This also prevented the Redis cluster from being destroyed in the event of a master failover.</p><h3>Key Takeaways</h3><p>In addition to writing clear and maintainable code, it’s equally important to understand the underlying storage layer that you are dealing with to produce efficient and bug-free code.</p><p>The following are some of the key learnings on Redis:</p><ul><li>Redis does not guarantee the consistency between master and its replica nodes when Lua scripts are used. You have to ensure that the behaviour of the scripts are deterministic to avoid data inconsistency.</li><li>Redis replicates the whole Lua script instead of the resulting commands to the replica. However, this is the default behaviour and you can disable it.</li><li>To save memory, Redis uses different representations for Hash. Your Hash object could be stored as a list in memory or a hashtable. This is not guaranteed to be the same across the master and its replicas.</li></ul><h3>Join Us</h3><p>Grab is the leading superapp platform in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across 428 cities in eight countries.</p><p>Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, <a href="https://grab.careers/">join our team</a> today!</p><p><em>Originally published at </em><a href="https://engineering.grab.com/tackling-ui-test-execution-time-imbalance-for-xcode-parallel-testing"><em>https://engineering.grab.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=77c76431d5d7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/grab/uncovering-the-truth-behind-lua-and-redis-data-consistency-77c76431d5d7">Uncovering the truth behind Lua and Redis data consistency</a> was originally published in <a href="https://medium.com/grab">Grab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>