Main

Episode 5: Implementing an XData server method that relies on another web service

In this episode, we dive into implementing an XData server method that depends on an external web service. The tutorial walks you through the intricacies of handling asynchronous calls, utilizing futures, and managing thread safety to ensure a smooth integration. Learn how to create a robust server method that efficiently interacts with external APIs, providing valuable insights into handling asynchronous operations and optimizing your XData server. NEXT EPISODES: Episode 6: https://youtu.be/6_Xt0uiwxSg Episode 7: https://youtu.be/78IYViPcFHg Episode 8: https://youtu.be/EmEAOGXCCwo Episode 9: https://youtu.be/XEDZFQlQir0 PREVIOUS EPISODES: Episode 1: https://youtu.be/lOyR0VyFl84 Episode 2: https://youtu.be/sRlxW0BMoR0 Episode 3: https://youtu.be/GfaO_OY-bVk Episode 4: https://youtu.be/cT4i5BWY4m4 SOURCE CODE: https://github.com/holgerflick/hiw.directionsweb MORE INFO: TMS WEB Core: https://www.tmssoftware.com/site/tmswebcore.asp TMS XDATA: https://www.tmssoftware.com/site/xdata.asp TMS FNC Maps: https://www.tmssoftware.com/site/tmsfncmaps.asp Website Holger: https://www.holgerscode.com/ CONNECT: Newsletter: https://www.tmssoftware.com/site/newsletter.asp Facebook: https://www.facebook.com/tmssoftware/ X: https://twitter.com/TMSsoftwareNews/ Threads: https://www.threads.net/@tmssoftware LinkedIn: https://be.linkedin.com/company/tmssoftware-com/ Instagram: https://www.instagram.com/tmssoftware/ ABOUT: TMS software, established in 1995, is a software development company specialized in: VCL, FMX, FNC, .NET, WEB component development Windows, Web, Android, iOS, macOS, Linux, Raspberry Pi development projects Training, consulting & custom project development

tmssoftwareTV

1 month ago

I hope you're ready for this. This is not only the most exciting part, but at the same time also the most difficult part. So, in one way, we can be happy that we learned something that is really, really interesting. But, on the other hand, it might be a little bit tedious because these things don't become clear right away, especially if you're not used to writing asynchronous code with multiple threads and are still taking your first steps in those kinds of fields. I myself have not written quit
e a lot of code in that department, so that's the reason why I literally sent the code snippet to the engineer that has conceived TMS XData and said, 'Hey, is this approach for multi-threading the correct approach? Is this something you would implement yourself?' And he agrees. In this case, that's how he would tackle it. The reason why it is so difficult will become clear right away as soon as you look at the code, how it needs to be done according to TMS FNC Maps. Just let me create another fo
rm in here. Form, and for whatever reason, this is the best-hidden. The form nowadays is the best-hidden component here. I think it's on Windows. And then I have to scroll down. There we go, VCL form. Let's drop a FNC Directions. Okay, poof, right here. And I have events here. For example, the getDirections, onGetDirections, onGetDirectionsResult. That's the key here. This event is triggered when direction results are retrieved. And that should ring a bell in your head because if I implement som
ething here, let's call this component 'directions.' Directions. And I go here, directions.getDirections. Right here. I would have to call this. I can specify an origin, a destination, and a callback. Or I don't specify it. I'll leave it nil. And then the component will call this event right here. And that's exactly the difficulty. Imagine that you write a method in your web service that calls getDirections, and then it has to wait until the result from the callback is retrieved. So at this poin
t, if we look at the code, we have, I mean, Wagner, the engineer that wrote XData, Wagner Longgraph, and I basically said there are two real options on how to implement this. And we decided for one of them, obviously. Maybe you come up with more, but those are the two that made the most sense to us. But looking at the implementation, we have the method getDrivingDirections. We get an origin and a destination as a string, and we need to return a route. That's what we need to use. By default, our
result is nil, meaning we don't have anything from Google yet. That is literally something that can happen if you specify, 'How can I go from Miami, Florida, to Seattle, Washington?' You might not get a reply in a certain amount of time. That is something you also have to remember. You're working with a Google web service on the backend of your backend, so to speak, and that might not deliver a reply. So nil is definitely a viable response for this thing. In order to identify the request that we
sent to Google, we need to generate some sort of ID. And in this case, I simply generated a UID, a unique ID. That's, to me, the most the easiest approach, so to speak. We do need this because there is no guarantee that getDrivingDirections is not called multiple times at the same time because multiple people can use your web application at the same time, and people can issue requests to your web service at the same time. That means there are parallel runs of this getDrivingDirections method. T
hat is just something that might happen. So you need to be able to distinguish later on which request you're dealing with. And this is not only the instance if you look at the multiple threads; this is also something that is built into the getDirections because there, the third parameter actually right here, this origin is the first, destination is the second, the callback is the third, the fourth is the ID of the request. So you can identify it where, well, if your callback gets called here, th
is is your callback right here with the request and the request result, there you also can read the ID. So you know which request it is. So if you have later on multiple requests at the same time, each request to Google, those are the requests we're assigning an ID to have a unique ID assigned to them. Everything separates into two tasks. The first task is to retrieve the information from Google. That's task number one. Task number two is to wait for Google to deliver this result, or if Google d
oes not deliver this result in a certain time, return 404 not found. That's the option we have here. Alternatively, you could also just shoot this getDirections request off and then instead of using some memory data structure to store the directions, you could also store this in a database or in a memory storage that is housed inside of your DirectionsManager class. But that would require you just to be very direct that this manager is always the same for all the different requests that you do,
that you do not use separate classes. And then you could have in your DirectionsManager a data structure that holds all the direction requests and routes that have been requested. But again, you see I'm going for the approach that is the least amount of work and is the easiest to understand at first. That's why we're not doing it. I'm not saying this is the most performant one, but this is one that works and is straightforward and is also, if you want to believe Wagner and me, the one that is re
liable. There's a lot of trust here, right? But in all seriousness, this has not been conceived in 5 minutes, 10 minutes, 20 minutes, 30 minutes. This is something where I sat down and built the structure of this method and thought, 'What could I use?' Because these tasks, of course, are a library that is bundled with Delphi, now the parallel library from Embarcadero. And after that, I did the thing. And this is the exact point where it gets a little bit tricky. Let's say you have this task to r
etrieve, and you create this task, and this task does nothing else but—don't let yourself get confused with the syntax. We just pass in a procedure inside of the parameter of the constructor of the task. So the task will execute this procedure when started. That's the content of it. And inside of the procedure, we call FDirections.getDirections. That is the component. Where does FDirections come from? It's defined in the class here. FDirections is the component TtmsFNCDirections. That's the comp
onent that I've been dropping on the form each time, and I initialize it as follows: TtmsFNCDirections.Create(nil). In this case, again, I want to free it myself. And then FDirections.APIKey := TAPIKey. TAPIKey is Google. I use my Google API key coming from this unit right here. And I am very much aware that I'm not showing it here. And then there might be people looking at the source code associated with this video and saying, 'Huh, he has the key in the history. He made this public.' And be as
sured, this key no longer exists that I used. That I checked into the source repository for my tests. I removed that key. I already replaced it with a different key. So even if I were to show it, I'm just not showing it because I don't know how YouTube analyzes these videos, and it's like, 'Oh, there's secret information in there.' So I'm not even going to show it. I'm saving myself some time to blur it. So that returns a string with the key, and the service is Google. So with that, the directio
n service is initialized. It's the same thing as if you drop it on the form and assign the properties. So this directions component, we call with getDirections, the origin, the destination. They, of course, come from the parameters of the method. And then, and this is where my head was completely screwed the first time I looked at this, of course, that is a callback. So we call getDirections and tell Delphi, 'Call this method with these parameters and with this callback done.' So Delphi immediat
ely executes the next line. And that is the end of this procedure. So the task will be terminated and done with. So if we were to say LTaskRetrieve.Wait, basically if we were to wait for this, it would finish right away. And it would be gone. LTaskRetrieve would be gone before the callback can even be called. So it would be done. Google would most likely process the result and then send it back on. There's nothing there anymore. If you're concerned about memory leaks, don't be because it's an IT
ask. So as soon as it goes away, Delphi will also handle the memory management. So there's nothing to be concerned about in that regard. It's just not going to work. Okay, there's, uh, you wait for something that concludes right away. So at that point, when you get after the waiting, you still don't have the result from the web service. So what we need to do is we need a task to wait. And this is, as I said, this is the one where the web service really, really is going to return a result. Altern
atively, you could say, 'Start, start this. Return nothing. Return the ID of this request and then build a second endpoint into your web service where you say, 'Get the data for this request.' Of course, you don't know how long this takes. You can ask, 'How long are you going to wait? 1 second, 2 seconds, 3 seconds?' And at that point, you need some sort of mechanism on the client where you ask for the same thing multiple times as long as the result is, 'Don't have an answer yet.' And then the a
nswer is either could be retrieved, could not be retrieved. And if it could be retrieved, you automatically return the route as well. And if it could not be retrieved, that's also an answer for the web client. However, that implies that your web client needs to issue requests with a timer or something on a regular basis. I don't like that because we're looking here at something of a maximum of 67-78 seconds to be retrieved. So it doesn't make any sense for me to build a client that polls on a re
gular basis. Do you have an answer yet? Do you have an answer yet? Do you have an answer yet? If you can implement it like this without doing that, basically this service is easier to consume than something like start the process and then retrieve the process. One example for a service that would benefit from having started off and then basically get regular updates is, for example, imagine you have an accounting system and you do payroll. And you have to generate the PDF documents for all your
employees as PDF documents. And then you have a web service which downloads a zip file of all those PDF documents. Just assume. Obviously, to create all those PDF files if you work in a company with 5,000 employees, it's not going to be 5, 10, 20 seconds. It's longer, most likely. And that means it does not make sense for the web server to wait in order to return the result right away. So instead, you would basically trigger the generation of the reports and then have a second endpoint that woul
d then basically ask which reports are ready or retrieve these reports. You would have a completely different interface to communicate with your web service than this. Okay, but we still don't have a result from the web service, right? We just called getDirections and we haven't looked at the callback yet. We also need a data structure that allows me to transfer data from this callback to here, to the outside world, basically. So what we do, we don't declare something locally. We say, 'FRoots is
a dictionary of roots identified by ID,' as you can see it right here. So we have a dictionary. The string is the ID of the route, of the request, and then we have a route associated with it. So going back here, we get the directions for an origin, a destination, and please call this callback with a request and a request result. If you wonder how I get these wonderful fingerprints right here, I don't remember these. This is not something I can store . I click on getDirections, say three hail ma
rys, you know, that DOI will find it. And in this case, it does. And that's how I get these things because then I can browse in the parameter list right here, a callback, click on the link, and here we go. TFnCDirections getDirections callback, reference to. And then here is my fingerprint. Of course, this is what I mentioned right at the beginning of this tutorial series. If you write your code for multiple platforms, you have these ifdefs everywhere. So that is the downside. That's why I opted
for client-server different. But here you see, this is exactly the fingerprint that I used. So going back here, we get back the request information and the request result, like status code, request result indicates if it's successful. Just be careful. A successful request can also mean that no route exists. The request was successful. Like if I ask for route from Florida to Dortmund, Germany, it's going to be successful. But the answer is there is no route. So we have to check a request and loo
k at the items inside of request. This time, a request, not a request result. A request. Items.Count. And if that's bigger than zero, we have more than one route. In this case, I only look at one route. I wanted to keep it simple. Obviously, I could loop and not only add one. I could create as many as are returned because with Google, you can also ask for alternatives. But by default, here, if you look at getDirections and go way back into the parameter list, there's something like here, alterna
tives, boolean. And that's by default false. So it only gives you one. And by default, it is the fastest, I think. So if you were to flip on alternatives to true, you get one that is the fastest, the shortest, and these kinds of things, with toll, without toll. You also can basically say avoid tolls to true, and these kinds of things. That's all configurable. This is just a minimal solution I'm presenting here. There's so much more to discover. And if you're interested to discover, think about t
he fact that there is a book, TMS Maps, from me. In the end, I'm going to present all the learning content that is available as well in addition to this tutorial. So if we have the item, or more than zero, and take the first one—I love inline variables here. I mean, you will see in the web client when we look at that how much easier and better the code becomes to read if you have this compared to not having it. I mean, imagine having to go, 'Where do you go now? Where do you declare a route?' We
ll, you have to declare it here because it's not a variable declaration for here either, right? Is it? It's like we have one function inside the function or procedure and another procedure. So having inline variables here makes the code more readable and is really just a pointed to the first route in this case. We also create a route of our data model, route, TRoute. And then for each step—and I named these Google steps because these are the steps inside of a route. So Google step is actually th
e data type that we get back from Google, right here. So we have all these funny things, coordinates with TMS FNC Maps coordinates, distance, durations. It's the same, but these are the TMS Map specific data types. So what I do now—and this is again, I said like think about the data model first. I say, 'LStep, TStep, create.' And then I can say, 'LStep, assign from a Google step.' Boom, initialized. Data model first. And then I say, 'LRoot, add step.' Yes, I created that for a reason so we can a
dd the step easily. And now the biggie, FRoots, add a request ID, a route. That's pretty straightforward. We want to, with the ID, and this in this case, we can no longer use it from here, right? Because we need the request that has been returned, and that is here in a request. It's returned to us as a parameter. And in LRoot is our TRoute. That's the one that we want to put on this dictionary with this key. Perfect. However, it might happen at the same time in a different thread. And remember,
if you write to the same memory address from different threads at the same time, it's going to blow up. So what do you do? By the way, I'm not going to even try to give any real-life analogies. Some people do that. I'm not. I'm just going to say never access the same memory space in different threads at the same time without making certain that you have a semaphore in place. So what we do is we call TMonitor.Enter, and we pass as the tag FRRoots, which is available in all the threads because it'
s like a field in the class that we use. So we lock it out. And what we do is we make sure that, as soon as we enter the semaphore, we add it to the dictionary. Then we make sure that the semaphore is being left with TMonitor.Exit. Very important, which makes certain that the two requests do not access the same data structure at the same time. Obviously, at this point, our information as it is in FRoots is also available anywhere in TDirectionsManager. Right? So what we need to do now is to have
a second task which waits for the ID to be added to the dictionary and then return it to the client. And that's what ALaskWait does. To make things even more complicated, I created a future. A future is a function that might return a value at some point in the future. Okay? And the type is of TRoute. So we have the fingerprint function CRoute, and this looks like in the code completion, looks like consun tun, right? And it's like one parameter and that is being returned. So the function has no
parameters. So the function has no parameters, but it returns a type TRoute, one of them. That's basically what's in there. Okay? So the function returns TRoute. I just explained it. That's again an anonymous function passed as a parameter just like with the other task. And what I do is I want to wait a maximum of 10 seconds. After 10 seconds, I want to bow out and say, 'Hey, we don't have a route.' Okay? That's the thing. So I'm going to try 20 times. So only if LStar smaller than 20, and each
iteration, I sleep for 500 milliseconds, which is half a second. And LRoot as only as long as we don't have a root, right? That's why I initialize it here with nil. And as soon as we have a root, this loop is left. The while condition is no longer met, and LStar smaller than 20. So that means when I'm done, I increase LStar. So what do I do in the semaphore again? I check if FRRoots contains the key. And if it does, I found my root. I can say LRoot is FRoots and remove this key because we no lon
ger need it. It's assigned to our request, so we no longer need it to be in the dictionary. And then we return the root. Right here, we say LFuture.SetResult. Basically, that's how you return a result from the future. That's how it works. So you create a future, set the result, and if you want to make certain that at some point, somebody is going to call LFuture.Value, right? And if the future is already complete, it's going to be available right away. If it's not complete, then it's going to wa
it for the completion. That's what the magic is about. So this is how it works. Let's put a break point here and a break point here and a break point here and in a client. Let's run this. Okay. Of course, it's going to work because it's not asynchronous, but let's do something very stupid, like LTaskWaitForRetrieve.Result. Why is this going to fail? Well, it's very easy. There's no route. This has not been initialized. The result here is nil. If I use result, the program would crash. However, th
e future is complete. Right? So LAskWaitForResult is going to give me a result right away, but it's going to be nil. So if I put a break point here, you will see exactly that. So if I F5 through, so wait for it. Boom. We go here. LRoute is nil. Right? So you're not going to have a route because it was nil. I didn't put anything in there. So if you, however, switch over to the client, we have a break point here, which gets hit first. And let's go through. You see we got a result from a future, bu
t it's nil. So what I'm doing now is I'm going to press F8, which is going to jump over the code and go to the next iteration, basically. That is going to be waiting again. And you will see that the future becomes complete at some point. So let's F8 through here. This is going to be hit, the break point. And then we do a LFuture.Value, which actually waits, you see. Now we're waiting. And as soon as this is complete, the result will be there. So if I'm going to press F8, you see it's complete,
and then we get the route. And now, LRoute is a valid route. We can inspect it. We have multiple steps inside of it, right? It's not nil anymore. And this is exactly what I meant with asynchronous code because this is not going to work synchronously. This is not going to work like this. You have to really think about what you're doing. Now let's set the asynchronous property back to true. Let's put a break point here and a break point here and put a break point here. And this time, when we run
it, we're going to see different things. Let's go back here. And this time, instead of just doing this, let's say we say, 'GetDirections', and then we do it like we did before. Origin, destination, callback, TTask.Create—let's go back here, make sure that we have TTask, and run TTask. Okay, and we pass in an anonymous function that calls LDirections.GetDirections. And let's make it more interesting. Let's say 'Sleep'. 1000, so 1 second. So it's going to be working asynchronously. We go back here
. Let's put a break point here, F5. And let's see how it behaves now. I'm not even going to F5 because you will see that it's not going to stop here. It goes through and immediately stops here because the result is nil. The future is not complete. So we press F8, and you see now it's waiting for the future to be complete. And as soon as it's complete, we get the result. This is how you would be doing it asynchronously. And now, the question, of course, arises, 'How do you make certain that the c
lient knows about the completion of this?' You know, in this case, obviously, I made it very simple by making sure that the client knows about the completion of it by simply waiting for the future. But what if I'm in an event-driven environment? That's something that you cannot do, right? So you have to do it different. But how you can do this differently and how you can implement a solution for that, I'm going to show you in the next video. Stay tuned.

Comments