Software Integration
Seamless communication — that, among other consequential advantages, is the ultimate goal when integrating your software. And today, integrating modern software means fusing various applications and/or systems — many times across distributed environments — with the common goal of unifying isolated data. This effort often signifies the transition of legacy applications to cloud-based systems and messaging infrastructure via microservices and REST APIs.So what's next? Where is the path to seamless communication and nuanced architecture taking us? Dive into our 2023 Software Integration Trend Report and fill the gaps among modern integration practices by exploring trends in APIs, microservices, and cloud-based systems and migrations. You have to integrate to innovate!
Distributed SQL Essentials
Advanced Cloud Security
Amazon Web Services (AWS) Lambda is an incredibly useful cloud computing platform, allowing businesses to run their code without managing infrastructure. However, the invocation type of Lambda functions can be confusing for newcomers. By understanding the key differences between asynchronous and synchronous invocations, you'll be able to set up your Lambda functions for maximum efficiency. Here's a deep dive into the mysteries of AWS Lambda invocation. Overview of the AWS Lambda Function Invocation Process The AWS Lambda Function Invocation Process begins when an event triggers the function. This event can come from a variety of sources, including HTTP requests, changes to data in an Amazon S3 bucket, or updates to a DynamoDB table. Once the event occurs, AWS Lambda automatically provisions and runs the necessary compute resources to process the request. There are two types of invocation methods in AWS Lambda: Synchronous and Asynchronous. The main difference between these two methods is the way in which they handle the response from the function. In synchronous invocation, the caller waits for the response from the function before continuing. This means that the function must execute completely before the caller can proceed. On the other hand, asynchronous invocation immediately returns a response to the caller, allowing it to continue with other tasks while the function executes in the background. Synchronous invocation is ideal for situations where a response is required immediately, such as when you're building a user-facing application or an API. Asynchronous invocation, on the other hand, is useful when you don't need an immediate response or when you have long-running tasks that require more time to execute. Regardless of which method you choose, AWS Lambda allows you to easily scale your functions to meet demand and pay only for the compute time that you consume. By choosing the right invocation method, you can optimize the performance of your Lambda functions and reduce costs. Asynchronous vs. Synchronous Invocation Let's take a closer look at some scenarios where you might choose to use synchronous or asynchronous invocation. Synchronous invocation is well-suited for tasks that require immediate feedback. For instance, if you're building an e-commerce website and a customer places an order, you'll want to know immediately whether the order was successful or not. In this case, you would use a synchronous invocation to ensure that the customer receives a response as soon as possible. Here is an example AWS CLI command to invoke a function named "aFunction" with input data from a file named input.json: Plain Text aws lambda invoke --function-name aFunction --payload file://input.json output.txt Asynchronous invocation, on the other hand, is great for tasks that involve long-running processes or batch jobs. For example, suppose that you're processing a large amount of data and need to perform some complex computations on it. In this case, you could use an asynchronous invocation to kick off the computation and return a response to the user while the computation continues to run in the background. For asynchronous invocation, Lambda places the event in a queue and returns a success response without additional information. A separate process reads events from the queue and sends them to your function. To invoke a function asynchronously, set the invocation type parameter to Event. Plain Text aws lambda invoke --function-name my-function --invocation-type Event --function-name aFunction --payload file://input.json output.txt Another example of when to use asynchronous invocation is when you're integrating multiple services together. Suppose that you have a workflow that involves several different services, each of which must be invoked in turn. By using asynchronous invocation, you can decouple the services from one another and allow them to execute independently, improving overall performance and scalability. In conclusion, choosing the right invocation method is critical for optimizing the performance and scalability of your AWS Lambda functions. By understanding the differences between synchronous and asynchronous invocation and using them appropriately, you can ensure that your applications are responsive, efficient, and cost-effective. Error Handling and Automatic Retries Another important feature of AWS Lambda is its built-in error handling and automatic retries. When a function encounters an error, Lambda automatically retries the execution using the same event data. This can be useful for transient errors such as network timeouts or temporary resource constraints. You can control the number of retries and the time between retries using the function's configuration settings. If the retries are unsuccessful, Lambda can either discard the event or send it to a dead-letter queue (DLQ) for further analysis. AWS recommends the use of Lambda Destinations. Lambda Destinations is a feature that allows you to define a destination for the asynchronous invocations of your AWS Lambda function. With Lambda Destinations, you can route failed invocations to a DLQ or another function for further processing. You can also send successful invocations to a queue or a stream for downstream processing. This feature provides more visibility and control over the behavior of your function, especially when handling asynchronous invocations at scale. By defining destinations for your function's invocations, you can monitor and troubleshoot issues more effectively and create more resilient serverless architectures. A DLQ is a feature in AWS that allows you to capture and store events or messages that could not be processed by a function. When a function fails to process an event, it can send the event to a DLQ instead of discarding it. This can be useful for debugging and troubleshooting purposes because you can analyze the failed events to determine the cause of the failure. In addition, you can set up alerts to notify you when events are sent to the DLQ, which can help you proactively identify and resolve issues. The DLQ can be configured for both synchronous and asynchronous invocations and can be used in conjunction with Lambda Destinations for more advanced error-handling scenarios. Overall, the dead letter queue is a powerful tool that can help you build robust and reliable serverless applications on AWS. How To Choose Between Asynchronous and Synchronous Invocation? Choosing the right invocation method between synchronous and asynchronous depends on the specific needs of your application. If your function is short-lived and you need immediate feedback from it, then synchronous invocation is the way to go. It provides a simple and straightforward way of executing functions and handling errors. However, if your function takes longer to execute or if you're integrating multiple services together, asynchronous invocation may be the better choice. It allows for greater scalability and performance since the caller is not blocked while long-running tasks are executed in the background. Additionally, you can use a combination of both synchronous and asynchronous invocation methods depending on your application's requirements. By understanding the strengths and weaknesses of each method, you can choose the right invocation method that optimizes the performance and scalability of your AWS Lambda functions.
Web3, blockchain technology, and cryptocurrency are all fascinating topics. The technology, applications, ecosystem, and the impact on society are all moving at incredible speeds. In this article, we will talk about learning web3 development from the point of view of a seasoned developer who is a total web3 newbie. We will look at the prerequisites for web3 development, use Python to access the blockchain via Infura, web3’s top API service, and go over a simple project for managing a wallet. How To Get Started Even though I’ve been coding since the late 1990s, I am truly a complete beginner in the web3 world. I’m no expert, so I won’t try to explain the fundamentals. There are a lot of great content guides and tutorials out there. I suggest starting with the Infura documentation, which is very comprehensive as well as comprehensible. There is also a community discord if you prefer a more interactive style of learning. Anyway, let’s start with some basics. We need an Infura account, a wallet to store our cryptocurrency, and we need some money to play with. Opening an Account With Infura Infura is a provider of blockchain APIs and developer tools. This means that if you want to access the blockchain, you don’t need to run a node yourself. Instead, you just access a friendly API, and Infura does all the heavy lifting for you. Infura is free and totally secure, as it doesn’t store your private keys and doesn’t have the ability to modify your transactions or replay them multiple times. You can open an account for free, and no credit card is required. Creating an Infura project A project is where things become interesting. Each project has an API key, which identifies it and allows you to use Infura. Follow the instructions here. Setting up a Crypto Wallet The next piece of the puzzle is a crypto wallet. This is where the rubber meets the road. In the blockchain environment, crypto wallets hold balances that are fully controlled by a set of digital keys. There is no such thing as personal ownership of an account. Each account has a public key — which is visible in the blockchain — and a private key that controls the account. Whoever holds a private key has total control of an account. You may also manage multiple accounts as a set of private keys. Wallets give you a secure way to manage your accounts/private keys as well as other benefits such as convenience, portability, and compatibility. Infura recommends MetaMask. You can install MetaMask as a browser extension. Great, we have a crypto wallet. Now let’s talk money! Getting Some Money The blockchain is not free. Crypto economics are way above my pay grade, but in simple terms, each transaction costs money. If you want to play on the blockchain, you need funds. Luckily for developers, there are test networks that let you get test money for free. You can’t exchange it for real money, but you can use it for developing and testing web3 applications. Speaking of which, there are different types of networks. Here, we will focus on the Ethereum blockchain. In this project, I used the testnet Sepolia. You can get test ETH from Sepolia by going to a faucet site. (ETH is Ethereum’s native cryptocurrency. You use it to pay for transactions on the Ethereum network. Test ETH is a necessity for Ethereum development.) A faucet site can transfer a small amount of testnet ETH to your wallet. Some faucets will require you to do some mining to earn your money, and some will gift you some money periodically. I had success with the ConsenSys Sepolia faucet which gives out .5 Sepolia ETH per day to an address. OK. We covered all the basics. Let’s check out the Infura API. Accessing the Infura API Infura provides a JSON-RPC API over HTTPS (REST) and WebSockets. It has several categories and you can read all about them here. Additionally, the Infura API supports multiple different networks. Each network has its own https endpoint that you use as a base URL when accessing the API. Here are the endpoints for Ethereum: Mainnet Ethereum Mainnet JSON-RPC over HTTPS— https://mainnet.infura.io/v3/<API-KEY> Ethereum Mainnet JSON-RPC over WebSocket— wss://mainnet.infura.io/ws/v3/<API-KEY> Goerli Ethereum Goerli Testnet JSON-RPC over HTTPS— https://goerli.infura.io/v3/<API-KEY> Ethereum Goerli Testnet JSON-RPC over WebSocket—wss://goerli.infura.io/ws/v3/<API-KEY> Sepolia Ethereum Sepolia Testnet JSON-RPC over HTTPS—https://sepolia.infura.io/v3/<API-KEY> Ethereum Sepolia Testnet JSON-RPC over WebSocket—wss://sepolia.infura.io/ws/v3/<API-KEY> Just to test that we can access the API, let’s get our wallet balance using curl. I stored the Infura API key and API key secret in environment variables called simply: INFURA_API_KEY and INFURA_API_KEY_SECRET. I also stored the public key of the MetaMask wallet in an environment variable called SEPOLIA_ACCOUNT. The curl command is: $ curl --user ${INFURA_API_KEY}:${INFURA_API_KEY_SECRET} \ -X POST \ -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["'"${SEPOLIA_ACCOUNT}"'","latest"],"id":1}' \ https://sepolia.infura.io/v3/${INFURA_API_KEY} a{"jsonrpc":"2.0","id":1,"result":"0x1d7e6e62f1523600"} As you can see, I have a HUGE balance of 0x1d7e6e62f1523600!!!! But no need to get too excited: the balance units are Wei. One ETH is equal to 10¹⁸ Wei. If we crunch the numbers, we can see that I have a little more than 2 ETH in my account. Of course, this is all testnet money. Note that I didn’t have to use my account’s private key to check my balance. Anyone can check the balance of any account in the blockchain. The balance of any account is not sensitive information. However, the identity of the account and the person that holds the private key is sensitive and confidential. All right… we had our fun with hitting the Infura API directly. Let’s do some coding. Web3 Development With Python The web3 ecosystem supports many programming languages. Infura APIs can be accessed from popular libraries in JavaScript (web3.js and ethers.js), Golang, and Python (web3.py). Choose Your Weapon: web3.py Although most of coding these days is in JavaScript/Node.js and Ruby, Python is great when learning a new topic. The web3.py library seems powerful, mature, and well-documented. So I decided to go with it. Choose Your Target: Wallet Manager The web3 world can be overwhelming: transactions, smart contracts, IPFS, DAO (decentralized autonomous organizations), defi (decentralized finance), and NFTs. I decided to pick a simple concept of a wallet manager for this web3 test project. The wallet manager is kind of a “hello web3 world” project because all it does is get your balance and send some money to a destination account. Since I got my money from a Sepolia faucet, I decided to give back by sending it some of the funds. Let’s check out the code. The web3-test dApp (Decentralized App) The code is available on Github here. (Special thanks to the-gigi!) I used Poetry to scaffold the application. The README provides step-by-step instructions to set up everything. Before we dive into the code, let’s run the program and see what happens: $ poetry run python main.py balance before transaction: 2.1252574454 send 20,000 gwei to 0xea4d57b2dd421c5bfc893d126ec15bc42b3d0bcd (Sepolia faucet account) balance after transaction: 2.125184945399832 As you can see, my balance was initially a little over 2 testnet ETH. Then, I sent 20,000 Gwei (which is 20 billion Wei) to the Sepolia faucet account that I got the money from in the first place. As you can see, it barely made a dent in my balance. This just shows what a tiny unit the Wei is. The code is pretty simple. There is just a single file called main.py. The file contains a main() function and a WalletManager class. Let’s start with the main() function, which is the entry point to the program. The main() function takes no command-line arguments or configuration files. Everything is hard-coded for simplicity. First, the function instantiates the WalletManager class, then it defines the public key of the Sepolia faucet account. Now, we get to action. The function obtains the balance of the wallet by invoking the get_balance() method of WalletManager, then it passes the requested unit (ether), and displays it on screen. Next, the function invokes the send_eth() method to send 20,000 Gwei to the target account. Finally, it gets and displays the balance again after the money has been sent. def main(): wm = WalletManager() sepolia_faucet_account = wm.w3.toChecksumAddress('0xea4d57b2dd421c5bfc893d126ec15bc42b3d0bcd') balance = str(wm.get_balance('ether')) print(f'balance before transaction: {balance}') print(f'send 20,000 gwei to {sepolia_faucet_account} (Sepolia faucet account)') wm.send_eth(sepolia_faucet_account, 20000, 'gwei') balance = str(wm.get_balance('ether')) print(f'balance after transaction: {balance}') if __name__ == '__main__': main() Let’s look at the WalletManager class. It has four methods: __init__(), __create_web3_instance() get_balance() sent_eth() Method 1: __init__() Let’s look at them one by one. The __init__()method, which is the constructor, first calls the __create_web3_instance() method and stores the result in a variable called w3. Then __init__() extracts a couple of environment variables and stores them. It continues to compute a couple of gas fees (which is the fuel the blockchain is running on) and the rewards that are given to the people that validate transactions. If you want to know more about gas and fees, read this. It also stores the chain ID, which identifies the Sepolia testnet (in this case). We will need this ID later when sending transactions to the Sepolia testnet. import base64 import os import web3 class WalletManager: def __init__(self): self.w3 = self.__create_web3_instance() self.account = os.environ['SEPOLIA_ACCOUNT'] self.account_private_key = os.environ['METAMASK_PRIVATE_KEY'] self.max_fee_per_gas = self.w3.toWei('250', 'gwei') self.max_priority_fee_per_gas = self.w3.eth.max_priority_fee self.chain_id = self.w3.eth.chain_id Method 2: __create_web3_instance() Let’s see what happens inside the __create_web3_instance() method. __create_web3_instance() is a static method because it doesn’t need any information from the WalletManager class. It gets the Infura API key and API key secret from the environment, and then it encodes them into a basic authentication token. It prepares the proper endpoint for our project on the Sepolia testnet, and then it instantiates a web3 object from the web3 library with all the information. This object will allow us to invoke the Infura API via a convenient Python interface (instead of constructing JSON-RPC requests and parsing the results). @staticmethod def __create_web3_instance(): infura_api_key = os.environ['INFURA_API_KEY'] infura_api_key_secret = os.environ['INFURA_API_KEY_SECRET'] data = f'{infura_api_key}:{infura_api_key_secret}'.encode('ascii') basic_auth_token = base64.b64encode(data).strip().decode('utf-8') infura_sepolia_endpoint = f'https://sepolia.infura.io/v3/{infura_api_key}' headers = dict(Authorization=f'Basic {basic_auth_token}') return web3.Web3(web3.HTTPProvider(infura_sepolia_endpoint, request_kwargs=dict(headers=headers))) Method 3: get_balance() Alright, next is the get_balance() method. It is an extremely simple method. It just invokes the w3.eth.get_balance() method of the web3 object and passes our account. The eth.get_balance() always returns the result in Wei, which is often too small. Our method provides us with the option to convert the result to another denomination like Gwei or Ether. It does it by invoking the w3.fromWei() method provided again by our web3 instance. Note that we didn’t have to use our private key to check the balance. balance = self.w3.eth.get_balance(selpytf.account) if unit != 'wei': return self.w3.fromWei(balance, unit) Method 4: send_eth() Last but not least is the send_eth() method. There is a lot going on here, so let’s break it into multiple blocks. First, send_eth() converts the amount to send to Wei (if necessary), and then it gets the transaction count of the account and stores it as nonce. The nonce allows us to overwrite pending transactions if needed. def send_eth(self, target_account, amount, unit='wei'): if unit != 'wei': amount = self.w3.toWei(amount, unit) nonce = self.w3.eth.get_transaction_count(self.account) Next, it constructs a transaction object. The most important fields are the from (the wallet’s account), the to (the transaction’s recipient) and the value (how much to send). Then, there are the fields that determine how much gas to pay. (The more gas, the more likely that validators will include the transaction,) The chainId identifies the network to run this transaction against and a couple of administrative fields (empty data and type). tx = {'nonce': nonce, 'maxFeePerGas': self.max_fee_per_gas, 'maxPriorityFeePerGas': self.max_priority_fee_per_gas, 'from': self.account, 'to': target_account, 'value': amount, 'data': b'', 'type': 2, 'chainId': self.chain_id} tx['gas'] = self.w3.eth.estimate_gas(tx) OK. We have a transaction, so can we send it? Not so fast. First, we need to sign it with our private key. This is what prevents other people from transferring money out of our account. Signing the transaction with the private key allows validators to confirm that the private key corresponds to the public key of the account. signed_tx = self.w3.eth.account.sign_transaction(tx, self.account_private_key) Now we can send the transaction as a raw transaction. This means that Infura never sees our private key, and it can’t change our transaction or direct our transfer to a different account. This is the magic of the blockchain in action. After sending the transaction, we get back a hash code and wait for the transaction to complete. If the status of the result is 1, then all is well. If not, the code raises an exception. tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) result = self.w3.eth.wait_for_transaction_receipt(tx_hash) if result['status'] != 1: raise RuntimeError('transaction failed: {tx_hash}') That’s all it takes to interact in a very basic, yet secure way with the blockchain. Conclusion: Start Your Web3 Journey With Infura Diving headfirst into the world of web3 can be intimidating, even for an experienced programmer. We’ve learned a lot; mostly, we’ve learned that we have a lot more to learn! Infura made it easy by providing a solid API, great guidance, and strong integration with other components of the ecosystem, like MetaMask and the web3.py library. If you are in a similar position and want to learn web3 development — or even want to start a web3 career — I highly recommend starting with Infura. See where the journey takes you!
Artificial intelligence has come a long way in recent years, particularly in natural language processing. Large-scale language models like OpenAI's GPT-3 have demonstrated an unprecedented ability to understand and generate human-like text. Prompt engineering, an emerging field in AI, aims to harness the potential of these models by crafting effective input prompts. In this article, I will introduce you to the world of prompt engineering, explain its importance, and offer practical tips on getting started. What Is Prompt Engineering? Prompt engineering is the art and science of formulating input prompts that guide AI models, such as GPT-3, in generating desired outputs. By fine-tuning the input, developers and AI enthusiasts can take advantage of the models' capabilities to create accurate, relevant, and context-aware results. Why Is Prompt Engineering Important? As powerful as large-scale language models are, they still require human guidance to generate meaningful and coherent outputs. Effective prompts are essential for obtaining desired results and minimizing the need for multiple iterations or manual intervention. Prompt engineering enhances AI models' overall efficiency and usability in various applications, from programming assistance to content generation. Getting Started With Prompt Engineering To embark on your prompt engineering journey, first, understand the AI model's capabilities and experiment with basic prompts. Emphasize specificity and clarity in your prompts, and try different structures for better results. Continuously improve your prompts and integrate ethical principles as part of prompt engineering. Understand the AI Model Before diving into prompt engineering, it is crucial to familiarize yourself with the AI model you will be working with. Spend some time researching the model's architecture, training data, and limitations. This understanding will help you craft better prompts and anticipate potential issues. Start with Simple Prompts When you're just starting, it's helpful to begin with simple and straightforward prompts. Experiment with basic questions, statements, or instructions, and observe how the model responds. This will give you a feel for how the model interprets and processes the input. Be Specific and Clear Large-scale language models are more likely to generate relevant outputs when provided with specific and clear prompts. Avoid ambiguity and provide as much context as necessary to guide the model toward the desired result. For example, instead of asking, "What is the best programming language?" you can ask, "What is the best programming language for web development?" Experiment With Different Prompt Structures The structure of your prompt can significantly impact the model's output. Experiment with different phrasings, question styles, and contexts. For example, you can try the following: Asking a question: "How do I create a Python function to calculate the factorial of a number?" Giving a command: "Explain how to create a Python function to calculate the factorial of a number." Providing examples: "Just like the addition and subtraction functions, create a Python function to calculate the factorial of a number." Refine Your Prompts As you gain experience with prompt engineering, you'll start to develop a sense of what works and what doesn't. Don't be afraid to iterate on your prompts and experiment with new approaches to improve the quality of the generated output. Consider Ethical Implications As you explore prompt engineering, remember to consider the ethical implications of using AI models. Be aware of potential biases in the model's training data and strive to create prompts that promote fairness, accountability, and transparency. Practical Applications of Prompt Engineering Prompt engineering can be employed in various domains, such as enhancing productivity, automating tasks, facilitating creativity, and simplifying complex processes. The examples provided are just a glimpse into the vast potential of prompt engineering. By mastering this skill, you can unlock countless possibilities and transform your approach to problem-solving. Programming Assistance: Craft effective prompts to generate code snippets, functions, or entire modules. This can help streamline the development process and save time. Content Generation: Use prompt engineering to create blog posts, articles, or marketing copy. By providing the right input, you can generate engaging and relevant content. Data Analysis: Harness the power of AI models to analyze complex datasets and extract valuable insights. Craft prompts that guide the model in identifying data trends, correlations, and anomalies. Customer Support: Implement prompt engineering to create AI-powered chatbots or virtual assistants that can answer customer inquiries and resolve issues. Design prompts that help the model understand the customer's problem and provide accurate solutions. Creative Writing: Explore the world of creative writing by generating stories, poems, or even scripts using prompt engineering. Provide the model with a theme, setting, or character to kick-start the creative process. Educational Resources: Use prompt engineering to create educational content, such as tutorials, lesson plans, or quizzes. Provide the model with the subject matter and the desired format to generate comprehensive and engaging learning materials. Prompt engineering is an exciting and promising field that enables beginners and experienced developers alike to harness the power of AI models effectively. By understanding the principles of prompt engineering and refining your skills, you can unlock the full potential of large-scale language models and transform the way you approach various tasks. As the field of AI continues to evolve, prompt engineering will play a vital role in shaping the future of technology and its applications across diverse domains.
I had a hard time solving the org.hibernate.type.descriptor.java.EnumJavaTypeDescriptor.fromOrdinal(EnumJavaTypeDescriptor.java:76)error that I got while reading the value from the database. Background While using the Spring Boot application, I saved some values in the database as an INT that I had to map back with the enum UserRole. The challenge: I used id and name in the enum, but instead of starting the id value from 0, I started the ID value from 1. This is the point the story started to challenge itself. The actual problem is instead of mapping the id of the enum while populating the object back to the Java object. It started reading the ordinal value. And I was feeling clueless even after debugging. Every blog that I was reading was suggesting to change the id to 0 so that the underlying exception (shown below) can be avoided. Java ERROR 13992 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2] with root cause java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2 ... My POJO mappings were: User POJO Java @Entity @Table(name = "USER") @Getter @Setter @NoArgsConstructor(force = true) public class User { @Id @Column(name = "ID") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "ROLE") @NotNull(message = "Role is required") @JsonAlias("roleId") private UserRole roleId; @Column(name = "NAME") @NotNull(message = "User Name is required") private Sting name; } UserRole POJO Java public enum UserRole { ADMIN(1, "Admin"), USER(2, "User"); private final int id; private final String name; private UserRole(final int id, final String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } public static UserRole valueOf(final Integer id) { if (id == null) { return null; } for (UserRole type : UserRole.values()) { if (type.id == id) { return type; } } return null; } } Solution When such a situation arises where you have to manipulate the DB values as per your enum, you can use the: javax.persistence.Converter Here's How! I added the following code to the User POJO: Java @Convert(converter = UserRole.UserRoleConverter.class) @Column(name = "ROLE") @NotNull(message = "Role is required") @JsonAlias("roleId") private UserRole roleId; And the below code to the UserRole POJO: Java @Converter(autoApply = true) public static class UserRoleConverter implements AttributeConverter<UserRole, Integer> { @Override public Integer convertToDatabaseColumn(UserRole attribute) { if (attribute == null) { throw new BadRequestException("Please provide a valid User Role."); } return attribute.getId(); } @Override public UserRole convertToEntityAttribute(Integer dbData) { return UserRole.valueOf(dbData); } } This converter maps the value fetched from the DB to the enum value by internally calling the valueOf() method. I was facing this error, and this approach solved the problem for me. I am pretty sure it will solve your problem too. Do let me know in the comments if you find the solution useful. :)
Are you looking to get away from proprietary instrumentation? Are you interested in open-source observability, but lack the knowledge to just dive right in? This workshop is for you, designed to expand your knowledge and understanding of open-source observability tooling that is available to you today. Dive right into a free, online, self-paced, hands-on workshop introducing you to Prometheus. Prometheus is an open-source systems monitoring and alerting tool kit that enables you to hit the ground running with discovering, collecting, and querying your observability today. Over the course of this workshop, you will learn what Prometheus is, what it is not, install it, start collecting metrics, and learn all the things you need to know to become effective at running Prometheus in your observability stack. Previously, I shared an introduction to Prometheus, installing Prometheus, and an introduction to the query language as free online workshop labs. In this article, you'll continue your journey exploring basic Prometheus queries using PromQL. Your learning path continues in this article with the exploration of a few basic PromQL queries. Note this article is only a short summary, so please see the complete lab found online here to work through it in its entirety yourself: The following is a short overview of what is in this specific lab of the workshop. Each lab starts with a goal. In this case, it is fairly simple: This lab dives into using PromQL to explore basic querying so that you can use it to visualize collected metrics data. You'll start off by getting comfortable selecting metrics and at the same time learning the basic definitions of metrics query terminology. Each query is presented for you to cut and paste into your own Prometheus expression browser and followed by an example output so that you see what the query is doing. These examples have been generated on a system running for several hours giving them more color and diversity when graphing data results that you might see if you are quickly working self-paced through this workshop. This doesn't take away from the exercise; it's meant to provide a bit more extensive example results than you might be seeing yourself. Next, you'll learn about how to filter your query results as you narrow the sets of data you're selecting using metric labels, also known as metric dimensions. You'll explore matching operators, filtering with regular expressions, dig into instant vectors, range vectors, learn about time-shifting, explore a bunch of ways to visualize your data in graphs, take a side step into the discussion around the functions without and by, and finally, learn how to use math in your queries. Selecting Data The very basic beginning of any query language is being able to select data from your metrics collection. You'll kick this lab off learning about the basic terminology involved with your metrics queries, such as: Metric name - Querying for all time series data collected for that metric Label - Using one or more assigned labels filters metric output Timestamp - Fixes a query single moment in the time of your choosing Range of time - Setting a query to select over a certain period of time Time-shifted data - Setting a query to select over a period of time while adding an offset from the time of query execution (looking in the past) Applying all of these you'll quickly start selecting metrics by name, such as the metric below from the services demo you set up in a previous lab: demo_api_request_duration_seconds_count This query, when entered in your Prometheus expression browser will result in something like the following query results: You'll continue to learn how to filter using labels, and how to narrow down your query results and metrics dimensions. After several iterations, you'll eventually find the data you're looking for with the following selection query filtering with multiple labels: demo_api_request_duration_seconds_count{method="POST", path="/api/foo", status="500"} The final results are much more refined than where you started: This wraps up the selection query section and sends you onward to having hands-on practical experience with selection queries. More Ways to Skin Your Data While using the equals operator is one way to select and filter data, there are more ways to approach it. The next section spends some time sharing how you can use regular expressions in your queries. This wraps up with you learning that up to now you've done queries selecting the single latest value for all time series data found, better known as an instant vector. You are now introduced to the concept of a range vector, or queries using functions that require a range of values. They have a duration specifier in the form of [number, unit]; for example, the following query selects all user CPU usage over the last minute: demo_cpu_usage_seconds_total{mode="user"}[1m] Resulting in the following display of all the values found over the last minute for this metric: Next, you'll learn how to take the range vectors and apply a little time-shifting to look at a window of data in the past, something that is quite common in troubleshooting issues after the fact. While this can be fun, one of the most common things you'll want to see in your data is how some metric has changed over a certain period of time. This is done using the rate function and you'll explore both this and its helper, the irate function. Below is the rate query: rate(demo_api_request_duration_seconds_count{method="POST"}[1m]) And the corresponding results in a graph: While these functions up to now have visualized counters in graph form, you have many other types of metrics you'll want to explore, such as gauge metrics. You'll spend time exploring all these examples and running more queries to visualize gauge metrics. This leads to a bit of a more complex issue. What do you do with queries where you want to aggregate over highly dimensional data in your queries? This requires simplifying and creating a less detailed view of the data results with functions like sum, min, max, and avg. These do not aggregate over time, but across multiple series at each point in time. The following query will give you a table view of all the dimensions you have at a single point in time with this metric: demo_api_request_duration_seconds_count{job="services"} The results are showing that this metric is highly dimensional: In this example, you are going to use the sum function, look across all the previous dimensions for a five-minute period of time, and then look across all captured time series data: sum( rate(demo_api_request_duration_seconds_count{job="services"}[5m]) ) The resulting graph shows the sum across highly dimensional metrics: You'll work through the entire list of aggregation functions, testing most of them in hands-on query examples using your demo installation to generate visualizations of your time series data collection. You'll finish up this lab by learning about how to apply arithmetic, or math to your queries. You'll sum it all up with a query to calculate per-mode CPU usage divided by the number of cores to find a per-core usage value of 0 to 1. To make it more interesting, the metrics involved have mismatched dimensions, so you'll be telling it to group by the one extra label dimension. It all sounds like a complex problem you have no idea how to solve? Don't worry, this entire lab builds you from the ground up to reach this point. You will be running this query to solve this problem and ready for the next more advanced query lab coming up! Missed Previous Labs? This is one lab in the more extensive free online workshop. Feel free to start from the very beginning of this workshop here if you missed anything previously: You can always proceed at your own pace and return any time you like as you work your way through this workshop. Just stop and later restart Perses to pick up where you left off. Coming Up Next I'll be taking you through the following lab in this workshop where you'll continue learning about the PromQL and dig deeper into advanced queries to gain more complex insights into your collected metrics. Stay tuned for more hands-on material to help you with your cloud-native observability journey.
“Set it and forget it” is the approach that most network teams follow with their authoritative Domain Name System (DNS). If the system is working and end-users find network connections to revenue-generating applications, services, and content, then administrators will generally say that you shouldn’t mess with success. Unfortunately, the reliability of DNS often causes us to take it for granted. It’s easy to write DNS off as a background service precisely because it performs so well. Yet this very “set it and forget it” strategy often creates blind spots for network teams by leaving performance and reliability issues undiagnosed. When those undiagnosed issues pile up or go unaddressed for a while, they can easily metastasize into a more significant network performance problem. The reality is that, like any machine or system, DNS requires the occasional tune-up. Even when it works well, specific DNS errors require attention so minor issues don’t flare up into something more consequential. I want to share a few pointers for network teams on what to look for when they’re troubleshooting DNS issues. Set Baseline DNS Metrics No two networks are configured alike. No two networks have the same performance profile. Every network has quirks and peculiarities that make it unique. That’s why knowing what’s “normal” for your network is important before diagnosing any issues. DNS data can give you a sense of average query volume over time. For most businesses, this is going to be a relatively stable number. There will probably be seasonal variations (especially in industries like retail), but these are usually predictable. Most businesses see gradual increases in query volume as their customer base or service volume grows, but this also generally follows a set pattern. It’s also important to look at the mix of query volume. Is most of your DNS traffic to a particular domain? How steady (or volatile) is the mix of DNS queries among various back-end resources? The answers to these questions will be different for every enterprise and may change based on network team decisions on issues like load balancing, product resourcing, and delivery costs. Monitor NXDOMAIN Responses NXDOMAIN responses are a clear indication that something’s wrong. It’s normal to return at least some NXDOMAINs for “fat finger” queries, standard redirect errors, and user-side issues that are likely outside of a network team’s control. NS1, an IBM Company’s recent Global DNS data report, shows that between 3-6% of DNS queries receive an NXDOMAIN response for one reason or another. Anything at or near that range is probably to be expected in a “normal” network setup. When you go over double digits, something bigger is probably happening. The nature of the pattern matters, though. A slow but steady increase in NXDOMAIN responses is probably a long-standing misconfiguration issue that mimics overall traffic volume. A sudden spike in NXDOMAINs could be either a localized (but highly impactful) misconfiguration or a DDoS attack. The key is to keep a steady eye on NXDOMAIN responses as a percentage of overall query volume. Deviation from the norm is usually a clear sign that something is not right — then it becomes a question of why it’s not right and how to fix it. In most cases, a deeper dive into the timing and characteristics of the abnormal uptick will provide clues about why it’s happening. NXDOMAIN responses aren’t always a bad thing. In fact, they could represent a potential business opportunity. If someone’s trying to query a domain or subdomain of yours and coming up empty, that could indicate that it’s a domain you should buy or start using. Watch Out for Exposure of Internal DNS Data One particularly concerning type of NXDOMAIN response is caused by misconfigurations that expose internal DNS zone and record data to the internet. Not only does this kind of misconfiguration weigh on performance by creating unnecessary query volume, but it’s also a significant security issue. Stale URL redirects are often the cause of exposed internal records. In the upheaval of a merger or acquisition, systems sometimes get pointed at properties that fade away or are repurposed for other uses. The systems are still publicly looking for the old connection but not finding the expected answer. The smaller the workload, the more likely it is to go unnoticed. Pay Attention to Geography If you set a standard baseline for where your traffic is coming from, it’s easier to discover anomalous DDoS attacks, misconfigurations, and even broader changes in usage patterns as they emerge. A sudden uptick in traffic to a specific regional server is a different kind of issue than a broader increase in overall query volume. Tracking your DNS data by geography helps identify the issue you’re facing and ultimately provides clues on how to deal with it. Check SERVFAILs for Misconfigured Alias Records Alias records are a frequent source of misconfigurations and deserve regular audits in their own right. I’ve found that an increase in SERVFAIL responses — whether a sudden spike or a gradual increase — can often be traced back to problems with alias records. NOERROR NODATA? Consider IPv6 NXDOMAIN responses are pretty straightforward — the record wasn’t found. Things get a little more nuanced when you see the response come back as NOERROR, but you also see that no answer was returned. While there’s no official RFC code for this situation, it’s usually known as a NOERROR NODATA response when the answer counter returns “0”. NOERROR NODATA means that the record was found, but it wasn’t the record type that was supposed to be there. If you’re seeing a lot of NOERROR NODATA responses, in our experience, the resolver is usually looking for an AAAA record. If you’ve got a lot of NOERROR NODATA responses, I’ve found that adding support for IPv6 usually fixes the problem. DNS Cardinality and Security Implications In the world of DNS, there are two types of cardinality to worry about. Resolver cardinality refers to the number of resolvers querying your DNS records. Query name cardinality refers to the number of different DNS names for which you receive queries each minute. Measuring DNS cardinality is important because it may indicate malicious activity. Specifically, an increase in DNS query name cardinality can indicate a random label attack or probing of your infrastructure at a mass level. An increase in resolver cardinality may indicate that you are being targeted with a botnet. If you suddenly see an increase in resolver cardinality, it’s likely an indication of some sort of attack. Conclusion These pointers should help you better understand the impact of DNS query behavior and some steps you can take to get your DNS to a healthy state. Feel free to comment below on any other tips you’ve learned throughout your career.
IBM App Connect Enterprise (ACE) has provided support for the concept of “shared classes” for many releases, enabling various use cases including providing supporting Java classes for JMS providers and also for caching data in Java static variables to make it available across whole servers (plus other scenarios). Some of these scenarios are less critical in a containerized server, and others might be handled by using shared libraries instead, but for the remaining scenarios there is still a need for the shared classes capability in containers. What Is the Equivalent of /var/mqsi/shared-classes in Containers? Adding JARs to shared classes is relatively simple when running ACE in a virtual machine: copying the JAR files into a specific directory such as /var/mqsi/shared-classes allows all flows in all servers to make use of the Java code. There are other locations that apply only to certain integration nodes or servers, but the basic principle is the same, and only needs to be performed once for a given version of supporting JAR as the copy action is persistent across redeploys and reboots. The container world is different, in that it starts with a fixed image every time, so copying files into a specific location must either be done when building the container image, or else done every time the container starts (because changes to running containers are generally non-persistent). Further complicating matters is the way flow redeploy works with containers: the new flow is run in a new container, and the old container with the old flow is deleted, so any changes to the old container are lost. Two main categories of solution exist in the container world: Copy the shared classes JARs into the container image during the container build, and Deploy the shared classes JARs in a BAR file or configuration in IBM Cloud Pak for Integration (CP4i) and configure the server to look for them. There is also a modified form of the second category that uses persistent volumes to hold the supporting JARs, but from an ACE point of view it is very similar to the CP4i configuration method. The following discussion uses an example application from the GitHub repo at https://github.com/trevor-dolby-at-ibm-com/ace-shared-classes to illustrate the question and some of the answers. Original Behavior With ACE in a Virtual Machine Copying the supporting JAR file into /var/mqsi/shared-classes was sufficient when running in a virtual machine, as the application would be able to use the classes without further configuration: The application would start and run successfully, and other applications would also be able to use the same shared classes across all servers. Container Solution 1: Copy the Shared Classes JARs in While Building the Container Image This solution has several variants, but they all result in the container starting up with the support JAR already in place. ACE servers will automatically look in the “shared-classes” directory within the work directory, and so it is possible to simply copy the JARs into the correct location; the following example from the Dockerfile in the repo mentioned above shows this: # Copy the pre-built shared JAR file into placeRUN mkdir /home/aceuser/ace-server/shared-classesCOPY SharedJava.jar /home/aceuser/ace-server/shared-classes/ and the server in the container will load the JAR into the shared classloader: Note that this solution also works for servers running locally during development in a virtual machine. It also means that any change to the supporting JAR requires a rebuild of the container image, but this may not be a problem if a CI/CD pipeline is used to build application-specific container images. The server may also be configured to look elsewhere for shared classes by setting the additionalSharedClassesDirectories parameter in server.conf.yaml. This parameter can be set to a list of directories to use, and then the supporting JAR files can be placed anywhere in the container. The following example shows the JAR file in the “/git/ace-shared-classes” directory: This solution would be most useful for cases where the needed JAR files are already present in the image, possibly as part of another application installation. Container Solution 2: Deploy the Shared Classes JARs in a BAR File or Configuration in CP4i For many CP4i use cases, the certified container image will be used unmodified, so the previous solution will not work as it requires modification of the container image. In these cases, the supporting JAR files can be deployed either as a BAR file or else as a “generic files” configuration. In both cases, the server must be configured to look for shared classes in the desired location. If the JAR files are small enough or if the shared artifacts are just properties files, then using a “generic files” configuration is a possible solution, as that type of configuration is a ZIP file that can contain arbitrary contents. The repo linked above shows an example of this, where the supporting JAR file is placed in a ZIP file in a subdirectory called “extra-classes” and additionalSharedClassesDirectories is set to “/home/aceuser/generic/extra-classes”: (If a persistent volume is used instead, then the “generic files” configuration is not needed and the additionalSharedClassesDirectories setting should point to the PV location; note that this requires the PV to be populated separately and managed appropriately (including allowing multiple simultaneous versions of the JARs in many cases)). The JAR file can also be placed in a shared library and deployed in a BAR file, which allows the supporting JARs to be any size and also allows a specific version of the supporting JARs to be used with a given application. In this case, the supporting JARs must be copied into a shared library and then additionalSharedClassesDirectories must be set to point the server at the shared library to tell it to use it as shared classes. This example uses a shared library called SharedJavaLibrary and so additionalSharedClassesDirectories is set to “{SharedJavaLibrary}”: Shared libraries used this way cannot also be used by applications in the server. Summary Existing solutions that require the use of shared classes can be migrated to containers without needing to be rewritten, with two categories of solution that allow this. The first category would be preferred if building container images is possible, while the second would be preferred if a certified container image is used as-is. For further reading on container image deployment strategies, see Comparing Styles of Container-Based Deployment for IBM App Connect Enterprise; ACE servers can be configured to work with shared classes regardless of which strategy is chosen.
Managing concurrent access to shared data can be a challenge, but by using the right locking strategy, you can ensure that your applications run smoothly and avoid conflicts that could lead to data corruption or inconsistent results. In this article, we'll explore how to implement pessimistic and optimistic locking using Kotlin, Ktor, and jOOQ, and provide practical examples to help you understand when to use each approach. Whether you are a beginner or an experienced developer, the idea is to walk away with insights into the principles of concurrency control and how to apply them in practice. Data Model Let's say we have a table called users in our MySQL database with the following schema: SQL CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, age INT NOT NULL, PRIMARY KEY (id) ); Pessimistic Locking We want to implement pessimistic locking when updating a user's age, which means we want to lock the row for that user when we read it from the database and hold the lock until we finish the update. This ensures that no other transaction can update the same row while we're working on it. First, we need to ask jOOQ to use pessimistic locking when querying the users table. We can do this by setting the forUpdate() flag on the SELECT query: Kotlin val user = dslContext.selectFrom(USERS) .where(USERS.ID.eq(id)) .forUpdate() .fetchOne() This will lock the row for the user with the specified ID when we execute the query. Next, we can update the user's age and commit the transaction: Kotlin dslContext.update(USERS) .set(USERS.AGE, newAge) .where(USERS.ID.eq(id)) .execute() transaction.commit() Note that we need to perform the update within the same transaction that we used to read the user's row and lock it. This ensures that the lock is released when the transaction is committed. You can see how this is done in the next section. Ktor Endpoint Finally, here's an example Ktor endpoint that demonstrates how to use this code to update a user's age: Kotlin post("/users/{id}/age") { val id = call.parameters["id"]?.toInt() ?: throw BadRequestException("Invalid ID") val newAge = call.receive<Int>() dslContext.transaction { transaction -> val user = dslContext.selectFrom(USERS) .where(USERS.ID.eq(id)) .forUpdate() .fetchOne() if (user == null) { throw NotFoundException("User not found") } user.age = newAge dslContext.update(USERS) .set(USERS.AGE, newAge) .where(USERS.ID.eq(id)) .execute() transaction.commit() } call.respond(HttpStatusCode.OK) } As you can see, we first read the user's row and lock it using jOOQ's forUpdate() method. Then we check if the user exists, update their age, and commit the transaction. Finally, we respond with an HTTP 200 OK status code to indicate success. Optimistic Version Optimistic locking is a technique where we don't lock the row when we read it, but instead, add a version number to the row and check it when we update it. If the version number has changed since we read the row, it means that someone else has updated it in the meantime, and we need to retry the operation with the updated row. To implement optimistic locking, we need to add a version column to our users table: SQL CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, age INT NOT NULL, version INT NOT NULL DEFAULT 0, PRIMARY KEY (id) ); We'll use the version column to track the version of each row. Now, let's update our Ktor endpoint to use optimistic locking. First, we'll read the user's row and check its version: Kotlin post("/users/{id}/age") { val id = call.parameters["id"]?.toInt() ?: throw BadRequestException("Invalid ID") val newAge = call.receive<Int>() var updated = false while (!updated) { val user = dslContext.selectFrom(USERS) .where(USERS.ID.eq(id)) .fetchOne() if (user == null) { throw NotFoundException("User not found") } val oldVersion = user.version user.age = newAge user.version += 1 val rowsUpdated = dslContext.update(USERS) .set(USERS.AGE, newAge) .set(USERS.VERSION, user.version) .where(USERS.ID.eq(id)) .and(USERS.VERSION.eq(oldVersion)) .execute() if (rowsUpdated == 1) { updated = true } } call.respond(HttpStatusCode.OK) } In this example, we use a while loop to retry the update until we successfully update the row with the correct version number. First, we read the user's row and get its current version number. Then we update the user's age and increment the version number. Finally, we execute the update query and check how many rows were updated. If the update succeeded (i.e., one row was updated), we set updated to true and exit the loop. If the update failed (i.e., no rows were updated because the version number had changed), we repeat the loop and try again. Note that we use the and(USERS.VERSION.eq(oldVersion)) condition in the WHERE clause to ensure that we only update the row if its version number is still the same as the one we read earlier. Trade-Offs Optimistic and pessimistic locking are two essential techniques used in concurrency control to ensure data consistency and correctness in multi-user environments. Pessimistic locking prevents other users from accessing a record while it is being modified, while optimistic locking allows multiple users to access and modify data concurrently. A bank application that handles money transfers between accounts is a good example of a scenario where pessimistic locking is a better choice. In this scenario, when a user initiates a transfer, the system should ensure that the funds in the account are available and that no other user is modifying the same account's balance concurrently. In this case, it is critical to prevent any other user from accessing the account while the transaction is in progress. The application can use pessimistic locking to ensure exclusive access to the account during the transfer process, preventing any concurrent updates and ensuring data consistency. An online shopping application that manages product inventory is an example of a scenario where optimistic locking is a better choice. In this scenario, multiple users can access the same product page and make purchases concurrently. When a user adds a product to the cart and proceeds to checkout, the system should ensure that the product's availability is up to date and that no other user has purchased the same product. It is not necessary to lock the product record as the system can handle conflicts during the checkout process. The application can use optimistic locking, allowing concurrent access to the product record and resolving conflicts during the transaction by checking the product's availability and updating the inventory accordingly. Conclusion When designing and implementing database systems, it's important to be aware of the benefits and limitations of both pessimistic and optimistic locking strategies. While pessimistic locking is a reliable way to ensure data consistency, it can lead to decreased performance and scalability. On the other hand, optimistic locking provides better performance and scalability, but it requires careful consideration of concurrency issues and error handling. Ultimately, choosing the right locking strategy depends on the specific use case and trade-offs between data consistency and performance. Awareness of both locking strategies is essential for good decision-making and for building robust and reliable backend systems.
Natural Language Processing (NLP) has revolutionized the way we interact with technology. With the rise of machine learning (ML) and artificial intelligence (AI), NLP has become an essential tool for developers looking to create intelligent, intuitive applications. However, incorporating NLP models into an application stack has not always been an easy task. Fortunately, new tools are now making it easier than ever before. These tools enable developers to incorporate NLP models into their application stack with ease. Where previously, developers had to train NLP models from scratch, which was time-consuming and required specialized expertise, tools such as OpenAI and Hugging Face are making it easier to build a powerful set of ML features using pre-trained models that can be easily incorporated into any application stack. One of the biggest advantages of these new developer tools is that they make coding and complex queries more accessible, even to those without advanced coding expertise. With the use of pre-trained models, developers don't need to start from scratch; they can simply use the models as a starting point and customize them to suit their needs. In addition, NLP models can usually be used 'out of the box' with some guidance for the model given in a 'prompt template' that provides context and response guidelines. This means that anyone can code and query, even the hard stuff, using simple, everyday language. How ML and NLP Play Together Unlike traditional rule-based systems, machine learning relies on a learning framework that allows computers to train themselves on input data. As a result, ML can use a wide range of models to process data, enabling it to understand both common and uncommon queries. Additionally, because machine learning models can continually improve from experience, they can handle edge cases independently without requiring manual reprogramming. On the other hand, NLP is a type of technology that employs machine learning algorithms to enable machines to understand human communication. By leveraging large datasets, NLP can create tools that understand the syntax, semantics, and context of conversations. The Benefits of NLP Integration One of the main benefits of integrating NLP into an application stack is that it can speed up the development of business applications. By using pre-trained models, developers can save time on training and testing, quickly incorporate NLP features into their applications, and get them to market faster. Let's start with task automation. NLP can automate tasks that would otherwise be time-consuming and costly. For example, the use of NLP can significantly speed up the process of analyzing large volumes of text data. Sentiment analysis, for instance, is a common NLP application that allows companies to quickly analyze customer reviews, social media posts, and other forms of user-generated content to identify patterns and trends. Another way that NLP can speed up development is by enabling developers to create more intuitive user interfaces. Natural language interfaces, such as chatbots and voice assistants, are becoming increasingly popular across a range of industries. These interfaces allow users to interact with applications using natural language rather than navigating complex menus and user interfaces. By incorporating NLP into these interfaces, developers can create more intuitive and user-friendly applications. NLP can also speed up the process of content creation, one of the most time-consuming processes in many businesses and one that requires significant human input and resources. However, with the use of NLP, developers can automate many aspects of content creation, such as content summarization, live transcripts and translations, and even the generation of new content. For example, a news outlet could use NLP to automatically generate summaries of news articles, allowing them to cover more stories in less time or put out summaries across their social media channels. Perhaps one of the greatest value-adds from NLP is that it can help speed up the data analysis and decision-making process. Using NLP to analyze data, developers can quickly identify patterns and trends and make real-time data-driven decisions. For instance, in the financial industry, NLP can be used to analyze market trends and predict stock prices, enabling traders to make informed investment decisions in real time. Incorporating NLP Into an Existing Tech Stack It's now easier than ever to incorporate NLP into an existing tech stack using new ML tools and frameworks such as OpenAI, Hugging Face, Spacy, or NLTK. It's important to choose a well-documented tool with a good track record and an active community of developers on hand to share knowledge and troubleshoot. Once a tool has been selected, it's time to move on to data preprocessing, which involves cleaning, tokenizing, and stemming the text data to standardize it and make it readable by NLP algorithms. For instance, "stemming" is a technique that reduces words to their root form – instead of using the words "running," "ran," and "runner, the root form "run" can be used on its own. These techniques can help to reduce the size of the vocabulary and improve the accuracy of NLP models. Then it's a case of selecting the right NLP model for a given use case. For instance, if a business is working on a sentiment analysis project, it might use pre-trained models such as BERT, GPT-2, or ULMFiT, which have already been trained on large volumes of conversational data. The benefits of integrating NLP into an application stack cannot be overstated and are key to creating intelligent, intuitive applications. Thanks to new tools, incorporating NLP capabilities into an existing tech stack is easier than ever. However, there are still some important decisions to be made along the way, such as which tools and frameworks to use and which NLP models are most appropriate to achieve a company's overall objectives. Many of these NLP models can be used "out of the box," but in order to capitalize on the democratization of NLP technology, businesses need to lay the groundwork by ensuring their data is ready, and the right developer tools are deployed.
I’ve coded in Java since the first beta. Even back then, threads were at the top of my list of favorite features. Java was the first language to introduce thread support in the language itself. It was a controversial decision back then. In the past decade, every language raced to include async/await, and even Java had some third-party support for that… But Java zigged instead of zagging and introduced the far superior virtual threads (project Loom). This post isn’t about that. I think it’s wonderful and proves the core power of Java. Not just as a language but as a culture. A culture of deliberating changes instead of rushing into the fashionable trend. In this post, I want to revisit the old ways of doing threading in Java. I’m used to synchronized, wait, notify, etc. But it has been a long time since they were the superior approach for threading in Java. I’m part of the problem. I’m still used to these approaches and find it hard to get used to some APIs that have been around since Java 5. It's a force of habit. There are many great APIs for working with threads which I discuss in the videos here, but I want to talk about locks which are basic yet important. Synchronized vs. ReentrantLock A reluctance I had with leaving synchronized is that the alternatives aren’t much better. The primary motivation to leave it today is that, at this time, synchronized can trigger thread pinning in Loom, which isn’t ideal. JDK 21 might fix this (when Loom goes GA), but it still makes some sense to leave it. The direct replacement for synchronized is ReentrantLock. Unfortunately, ReentrantLock has very few advantages over synchronized, so the benefit of migrating is dubious at best. In fact, it has one major disadvantage. To get a sense of that, let’s look at an example. This is how we would use synchronized: Java synchronized(LOCK) { // safe code } LOCK.lock(); try { // safe code } finally { LOCK.unlock(); The first disadvantage of ReentrantLock is the verbosity. We need the try block since if an exception occurs within the block, the lock will remain. Synchronized handles that seamlessly for us. There’s a trick some people pull of wrapping the lock with AutoClosable which looks roughly like this: Java public class ClosableLock implements AutoCloseable { private final ReentrantLock lock; public ClosableLock() { this.lock = new ReentrantLock(); } public ClosableLock(boolean fair) { this.lock = new ReentrantLock(fair); } @Override public void close() throws Exception { lock.unlock(); } public ClosableLock lock() { lock.lock(); return this; } public ClosableLock lockInterruptibly() throws InterruptedException { lock.lock(); return this; } public void unlock() { lock.unlock(); } } Notice I don’t implement the Lock interface, which would have been ideal. That’s because the lock method returns the auto-closable implementation instead of void. Once we do that, we can write more concise code such as this: Java try(LOCK.lock()) { // safe code } I like the reduced verbosity, but this is a problematic concept since try-with-resource is designed for the purpose of cleanup, and we reuse locks. It is invoking close, but we will invoke that method again on the same object. I think it might be nice to extend the try with resource syntax to support the lock interface. But until that happens, this might not be a worthwhile trick. Advantages of ReentrantLock The biggest reason for using ReentrantLock is Loom support. The other advantages are nice, but none of them is a “killer feature.” We can use it between methods instead of in a continuous block. This is probably a bad idea as you want to minimize the lock area, and failure can be a problem. I don’t consider that feature as an advantage. It has the option of fairness. This means that it will serve the first thread that stopped at a lock first. I tried to think of a realistic non-convoluted use case where this will matter, and I’m drawing blanks. If you’re writing a complex scheduler with many threads constantly queued on a resource, you might create a situation where a thread is “starved” since other threads keep coming in. But such situations are probably better served by other options in the concurrency package. Maybe I’m missing something here… lockInterruptibly() lets us interrupt a thread while it’s waiting for a lock. This is an interesting feature, but again, hard to find a situation where it would realistically make a difference. If you write code that must be very responsive for interrupting, you would need to use the lockInterruptibly() API to gain that capability. But how long do you spend within the lock() method on average? There are edge cases where this probably matters but not something most of us will run into, even when doing advanced multi-threaded code. ReadWriteReentrantLock A much better approach is the ReadWriteReentrantLock. Most resources follow the principle of frequent reads, and few write operations. Since reading a variable is thread-safe, there’s no need for a lock unless we’re in the process of writing to the variable. This means we can optimize reading to an extreme while making the write operations slightly slower. Assuming this is your use case, you can create much faster code. When working with a read-write lock we have two locks, a read lock as we can see in the following image. It lets multiple threads through and is effectively a “free for all.” Once we need to write to the variable, we need to obtain a write lock, as we can see in the following image. We try to request the write lock, but there are threads still reading from the variable, so we must wait. Once the threads are finished reading, all reading will block, and the write operation can happen from a single thread only, as seen in the following image. Once we release the write lock, we will go back to the “free for all” situation in the first image. This is a powerful pattern that we can leverage to make collections much faster. A typical synchronized list is remarkably slow. It synchronizes over all operations, read or write. We have a CopyOnWriteArrayList, which is fast for reading, but any write is very slow. Assuming you can avoid returning iterators from your methods, you can encapsulate list operations and use this API. E.g., in the following code, we expose the list of names as read-only, but then when we need to add a name, we use the write lock. This can outperform synchronized lists easily: Java private final ReadWriteLock LOCK = new ReentrantReadWriteLock(); private Collection<String> listOfNames = new ArrayList<>(); public void addName(String name) { LOCK.writeLock().lock(); try { listOfNames.add(name); } finally { LOCK.writeLock().unlock(); } } public boolean isInList(String name) { LOCK.readLock().lock(); try { return listOfNames.contains(name); } finally { LOCK.readLock().unlock(); } } StampedLock The first thing we need to understand about StampedLock is that it isn’t reentrant. Say we have this block: Java synchronized void methodA() { // … methodB(); // … } synchronized void methodB() { // … } This will work. Since synchronized is reentrant. We already hold the lock, so going into methodB() from methodA() won’t block. This works with ReentrantLock, too, assuming we use the same lock or the same synchronized object. StampedLock returns a stamp that we use to release the lock. Because of that, it has some limits. But it’s still very fast and powerful. It, too, includes a read-and-write stamp we can use to guard a shared resource. But unlike the ReadWriteReentrantLock, it lets us upgrade the lock. Why would we need to do that? Look at the addName() method from before… What if I invoke it twice with “Shai”? Yes, I could use a Set… But for the point of this exercise, let’s say that we need a list… I could write that logic with the ReadWriteReentrantLock: Java public void addName(String name) { LOCK.writeLock().lock(); try { if(!listOfNames.contains(name)) { listOfNames.add(name); } } finally { LOCK.writeLock().unlock(); } } This sucks. I “paid” for a write lock only to check contains() in some cases (assuming there are many duplicates). We can call isInList(name) before obtaining the write lock. Then we would: Grab the read lock Release the read lock Grab the write lock Release the write lock In both cases of grabbing, we might be queued, and it might not be worth the extra hassle. With a StampedLock, we can update the read lock to a write lock and do the change on the spot if necessary as such: Java public void addName(String name) { long stamp = LOCK.readLock(); try { if(!listOfNames.contains(name)) { long writeLock = LOCK.tryConvertToWriteLock(stamp); if(writeLock == 0) { throw new IllegalStateException(); } listOfNames.add(name); } } finally { LOCK.unlock(stamp); } } It is a powerful optimization for these cases. Finally I cover many similar subjects in the video series above; check it out and let me know what you think. I often reach for the synchronized collections without giving them a second thought. That can be reasonable sometimes, but for most, it’s probably suboptimal. By spending a bit of time with thread-related primitives, we can significantly improve our performance. This is especially true when dealing with Loom, where the underlying contention is far more sensitive. Imagine scaling read operations on 1M concurrent threads… In those cases, the importance of reducing lock contention is far greater. You might think, why can’t synchronized collections use ReadWriteReentrantLock or even StampedLock? This is problematic since the surface area of the API is so big it’s hard to optimize it for a generic use case. That’s where control over the low-level primitives can make the difference between high throughput and blocking code.
[DZone Survey] What’s Your Superpower?
April 20, 2023 by
Benefits of React V18: A Comprehensive Guide
April 20, 2023 by
April 20, 2023 by
April 20, 2023 by CORE
Data Encryption: Benefits, Types, and Methods
April 20, 2023 by
Maximizing the Potential of LLMs: Using Vector Databases
April 20, 2023 by
Data Encryption: Benefits, Types, and Methods
April 20, 2023 by
April 20, 2023 by CORE
Static Proxies or Rotating Proxies: Which One Should You Choose?
April 20, 2023 by
April 20, 2023 by CORE
April 20, 2023 by CORE
Benefits of React V18: A Comprehensive Guide
April 20, 2023 by
April 20, 2023 by CORE
AWS: Pushing Jakarta EE Full Platform Applications to the Cloud
April 20, 2023 by CORE
GitStream vs. Code Owners vs. GitHub Actions
April 19, 2023 by CORE
Data Encryption: Benefits, Types, and Methods
April 20, 2023 by
Maximizing the Potential of LLMs: Using Vector Databases
April 20, 2023 by
Benefits of React V18: A Comprehensive Guide
April 20, 2023 by