• last year
High-Level concurrency in this context, only means working with concurrency and kotlin coroutines with extra features provided by Project Arrow. High level concurrency allows us to create much more idiomatic Kotlin code that follows standards, good practices and is pure. This video is the source of all the short videos created for the playlist about the same topic. For those of you who enjoy and have asked for the long form version here it is! This video caters for those of you who like to watch an explanation without interruptions and without prominent music.


---

The playlist can be found here:

- https://www.youtube.com/playlist?list=PLw1b-aiSLRZjd6C99OwcfNa_MNpDqVn7Z

---

The repo that supports the cases is located here:

- https://github.com/jesperancinha/asnsei-the-right-waf

---

As a short disclaimer, I'd like to mention that I'm not associated or affiliated with any of the brands eventually shown, displayed, or mentioned in this video.

---

All my work and personal interests are also discoverable on other different sites:

- My Website - https://joaofilipesabinoesperancinha.nl/
- Reddit - https://www.reddit.com/user/jesperancinha
- Credly - https://www.credly.com/users/joao-esperancinha/badges
- Pinterest - https://nl.pinterest.com/jesperancinha/
- Instagram - https://www.instagram.com/joaofisaes/
- Facebook - https://www.facebook.com/joaofisaes/
- Spotify - https://open.spotify.com/user/jlnozkcomrxgsaip7yvffpqqm
- Medium - https://medium.com/@jofisaes
- Daily Motion - https://www.dailymotion.com/jofisaes
- Tumblr - https://www.tumblr.com/blog/jofisaes

---

If you have any questions about this video please put a comment in the comment section below and I will be more than happy to help you or discuss any related topic you'd like to discuss.

If you want to discover more about my open-source work please visit me on GitHub at:

- GitHub - https://github.com/jesperancinha
Transcript
00:00Back in 2020, I developed a project called Good Story, where I investigated coroutines
00:17and how they are being applied to Kotlin coroutines and Java Virtual Threads with continuation
00:22principles.
00:23Back then, Java Virtual Threads weren't available in any LTS version except for an
00:27alternative version of the JDK 19 under Project Loom.
00:30And so, the only continuations implementation available between Java and Kotlin for production
00:35was Kotlin coroutines.
00:37But one of the things that I quickly realized is that in spite of Kotlin coroutines being
00:40a lot of fun, they can also get very complicated, especially when you consider which coroutine
00:45scope we want to use, what elements of the coroutine context we want to use, and depending
00:49on how we would want to join the different results of different computations, Kotlin
00:53coroutines could become really complicated in our code.
00:56And one of the easiest way out to solve these problems would be to create our own library.
01:00That library is probably not needed anymore, thanks to something that is now called High
01:04Level Concurrency, which is now readily available straight out of the box in Project Arrow.
01:09And that is what we are going to talk about in this video series.
01:12Project Arrow may not offer all the features you need, although it may actually do just
01:16that.
01:17So the best thing to do is to have a look at the example.
01:19So let's have a look at it.
01:20And first, let's talk about the example.
01:22And then let's run the full example in one go and examine the logs.
01:27So this example is about creating books and the interaction between the books and the
01:30library and the customers.
01:32So keeping that in mind, that is the context of our theme for this example.
01:36When we start to have a look at our code, we can see that the first thing I have is
01:39the random creation of a random ID.
01:42And here I'm just using a val to do that.
01:44And it has one getter that simply creates a random ID.
01:47It's important to notice because it will be reused in our example.
01:50Then let's go straight into our main function where the example begins.
01:53And we start with a run block in context, because we are going to need to run this under
01:58a coroutine scope.
01:59And so the first thing we do in our example is we create an ID that is going to be reused
02:03in all our calls.
02:04The examples have numbers in them, and they are sequentially programmed so that we can
02:08get a better understanding of how they work.
02:10This might change in the future.
02:12So keep that in mind.
02:13In the first example, we are simply creating a book.
02:15We are creating the book under a coroutine scope, and we are going to measure how long
02:19it takes to create a book.
02:20Why are we going to do that?
02:21Let's have a look.
02:22We are creating a book using two resources.
02:24One is the name of the book, and the other is the ISD number.
02:27This simulates the idea that we need to create a book using a name that is located in one
02:31system and a ISD number in another.
02:34To simulate a possible delay with these systems, I'm saying that before I get the name and
02:38before I get the ISD number, it will take one second to do that.
02:41Then I am merging these two results into one single book result.
02:45The key to this mechanism is this par zip function.
02:50This par zip function comes with Project Arrow.
02:51Let's have a look at how it works.
02:52If we go here to par zip, we can see that par zip is a suspend function that accepts
02:57three crossing line suspend functions, and then it will call another par zip and so on
03:01with a default dispatcher.
03:03If you remember the default dispatcher, it works with CPU bound operations, which is
03:08exactly what we need here because the operation that is going to take place is only a merge
03:12of the two values under a specific function that we are creating for it.
03:16It is not supposed to make input output operations.
03:18We can do that as well, but maybe that's not a good idea to do that for our merge slash
03:23zip operation.
03:24But let's have a look even further at how this is implemented in Project Arrow.
03:28Inside this other overload of the par zip function, we can see that it receives as input
03:35parameters the different suspend functions that we are creating, different three crossing
03:40line functions.
03:41And at the end, it's going to the last function, which is the zip operation that we are passing
03:47as argument.
03:48And now we can see the interesting bit about this function, which is this async.
03:52We are calling two async functions and we are waiting for the operation to complete.
03:56And then we are simply calling the operation f that will then put those two results together
04:03and create a book for us.
04:04It seems simple, but this didn't exist in Kotlin coroutines out of the box.
04:09So that means that what we used to do in this case is simply create an alternative library
04:13to reuse this function in different projects.
04:15But let's go back to our racist service and think how long this operation would take.
04:19One second, right?
04:20Because we have a delay and we have another delay.
04:22But let's look at that later.
04:23Now let's look at the second example, or in this case, example 1.2.
04:26In this case, we are interested in getting the result if everything fails.
04:29So let's have a look at it.
04:30When we go to try to create a book, it does the same par zip call, but it is throwing
04:35an exception for both cases, for the name and for the ISDN number.
04:40So what do you think happens here?
04:41Will an exception be thrown and will this be manageable?
04:44For this specific implementation, the sad truth is that the exception will be thrown.
04:47The exception just gets thrown and the program would stop the execution here, except that
04:52I am doing a run catching right over here.
04:54So here I make sure that the program stays running.
04:57But there is a solution for this.
04:58In this example over here, we do a try to create either book.
05:02It's another function that is going to simulate the fail, but it's going to do that in an
05:06intelligent way.
05:07Let's have a look at it.
05:08What we do with this function is we create an either, and an either, it creates a race
05:14scope.
05:15And the race scope is what allows us to return an either.
05:19And remember, on the left side of the either are usually our fails, and on the right side
05:24of the either is usually our successes.
05:26This is what this either does.
05:28It allows us to raise a problem when something fails.
05:33In here, we will get a fail of the name, but we will get a success of the ISDN number.
05:39But there is a problem here.
05:40The problem with this one specifically is that we will only get an actual fail.
05:44Or maybe the question is, but what do you think would happen here?
05:46Do we get an either with a fail?
05:48Or do we get an either with a success?
05:51Or do we get an either with a fail and a success?
05:53But one thing we can be sure here is that an error is going to be thrown one way or
05:57the other.
05:58We'll look at that later in our example.
06:00Let's continue.
06:01The next example is about ParMap.
06:02And ParMap allows us to do something like ParZip, but instead of combining lists, let's
06:07see how that works.
06:08This example is about creating associated titles to a book.
06:11So we've got a lot of books, and one of those books will have other books associated to
06:16them.
06:17But remember that this is only a simulation.
06:19Let's have a look at the GetAssociatedTitles implementation and see how ParMap actually
06:24works.
06:25Inside this method, we can see that it is still a suspend function, and we are calling
06:30this method GetAssociates and then using a ParMap.
06:33What this is doing is getting a name per associate ID that we give it.
06:38And it's going to do that for as many times as associates there are.
06:42So let's see the GetAssociated method first.
06:45So the GetAssociates method returns a list of five IDs that we are going to use to call
06:50the ParMap.
06:51This means that we are going to call ParMap five times and then getting the name for it.
06:55And notice that I'm putting a delay there.
06:57Similarly to what we have seen with ParZip, this delay is to simulate how long it takes
07:01to get the actual name of that associate, which is just the same method as before.
07:06It's just a book with the ID that we are passing in and then a random ID.
07:11That is the name of the book that we are giving in.
07:13And in the case that we get two different dashes, we remove that and replace it by one.
07:17That's one simple way of creating a random name.
07:19So let's go back to our original call.
07:21And here we can now see that this will get us the names of all of the different associates.
07:26And we can now assume that this is going to happen in parallel in an asynchronous way.
07:30And this is just going to take one second.
07:33Now let's see what happens in ParMap.
07:35We see that we get a coroutine context straight out of the box, meaning that we can put our
07:39own coroutine context if we need.
07:41And then we see that ParMap also accepts as a parameter the transform function.
07:45This is the function we have seen before that will get us our name with a one second delay.
07:49And then we see that using a coroutine scope, it performs our mapping by invoking our transform
07:54function and then awaiting for a call that's being made in an asynchronous way.
08:00And the fact that we use await all means that all of these asynchronous calls together will
08:04take as long as the longest running call will take.
08:07Can you predict at this point how long this operation is going to take?
08:10One second, right?
08:11We'll see in the example if that is so.
08:12But let's go back up to our program and see the next example.
08:16How do we go about error handling when our ParMap function fails?
08:19Let's have a look at this example.
08:21In our getAssociatedFailedTitles, we are still mapping our associates with their names, only
08:26that now I am raising an error.
08:28And if you look at it closely, we can see that we are using this either over here, much
08:32in the same way as in the ParZip.
08:35And in here, we can also see that we have a raise scope.
08:39And so we can raise an error and return an either.
08:42This is not anything new per se.
08:45So it's just the same way.
08:47But there is something interesting about ParMap, and one particular implementation of ParMap
08:51that can make this work much easier for us.
08:55Because with this function, we may get an error, but our function will immediately fail.
09:00And maybe we want to get all of the different sequential errors that happen for every single
09:05mapping that we are doing from the ID to the associate name.
09:08And to do that, there is actually a ParMap that is a cumulative using either as well
09:14and using a raise scope as well, which comes straight out of the box as well in Project
09:19Arrow.
09:20And that is called ParMap or accumulate.
09:23For this one, we don't need to use an either, and we don't need to implicitly create a raise
09:28scope for it.
09:29This one will do that already for us, but we will not return any either.
09:33It will return an either with a list of successes and a list of errors.
09:37If we look closely, we see that we are still calling the associates function.
09:41We are also calling the ParMap or accumulate, which is the new function, and then we are
09:46calling a delay.
09:47And then finally, we do a raise simulating an error that may occur when we get the name.
09:52We are going to get a list of errors, and the type that we get back is a non-empty list
09:58or a list of strings.
10:00Notice that both of them are a list of strings.
10:02One of the things that you may find surprising in this code in the way that ParMap or accumulate
10:07work is the fact that we are returning an either of two lists, but one of the lists,
10:13especially the error list on the left, is a non-empty list.
10:16And this may sound confusing because some of you may think that non-empty list, does
10:20that mean that we have to have an error in our return value?
10:22Sounds strange, right?
10:23This non-empty list can be read in different ways.
10:25And one of them is that if we get an error, we will get a list with at least one element
10:31on it.
10:32And that the either that's going to be returned is going to be a left with all the errors
10:35that are being set.
10:37So let's say that one of these mappings is successful and the remaining ones fail.
10:40How would this look like in our return value?
10:42In that case, since one error has occurred, we are guaranteed to give back a non-empty
10:47list because we have one error in it and our result will be a left with a list of all of
10:52the errors that have occurred.
10:53And that way we can automatically process our code as an error and we don't have to
10:58think about the remaining results because in this case, it doesn't matter.
11:02We want this operation to cumulatively work successfully and in that case, it doesn't.
11:05And that's where this non-empty list comes from, making this a much better way to handle
11:11errors and process successful results.
11:13This non-empty list guarantees a lot of things.
11:16And among them, the best ones are the predictability of our code, not having to deal with ifs to
11:20check if the list appears that the return is empty or not.
11:23And it removes the error prone capabilities of checking if the list is empty or not.
11:29In our final example, we are looking at race.
11:31And race allows us to start multiple different coroutines and use only the result of the
11:36quickest one.
11:37So here we are looking at FetchBook and the idea is that we are fetching a book from one
11:41library or the other, wherever the book is available.
11:44And the first one to return a book is the first book that we are going to use and the
11:47other one can stay in the library.
11:49Let's have a look at the method and the functions that we use to create this feature.
11:53In the FetchBook, we are calling race and race accepts two functions as input parameters.
11:58The two arguments that we are defining are two functions.
12:01The first function gets a book from a library in Gouda and the other one gets a book from
12:05a library in Ollão.
12:06So let's see what is the result.
12:08So let's see what they actually do in their implementation.
12:10You probably already guessed it.
12:12They do exactly the same thing.
12:13But the idea is that there are two different coroutines that are being launched that can
12:17potentially do different things, have different delays, and then result in different outcomes.
12:22The two functions are implemented in exactly the same way.
12:25And what they do is they both delay randomly from 0 to 100 milliseconds.
12:29That means that the quickest one will be random per execution of this code.
12:34And the book is, of course, the silence of the kittens with the same ID and the same
12:38ISTN number.
12:40The first library is Gouda and the second one is Ollão.
12:42For the first library, we configured the name Gouda and the second library, we configured
12:46the name Ollão.
12:47What happens here is that when we call the function raceN, it will kickstart the two
12:51different functions.
12:52And then by doing merge, we will get the waiting result of the race between the two calls.
12:58So we can see here that raceN receives two input parameters called fa and fb, and both
13:02of them are two different crossing-line functions with the receiver coroutine scope.
13:06And then, and then, there's a new call that will launch the two different functions in
13:11synchronously over here, and then it will await for them and make sure that the winner
13:17is returned and the loser, in this case, gets cancelled.
13:21So it uses coroutine cancellation to cancel the loser of this race.
13:25We can see here how raceN can be very useful, but what happens if it fails?
13:29How can we deal with errors?
13:31So how do we go about with error handling with raceN?
13:34There doesn't seem to be a way to do that directly.
13:36There are two ways.
13:37One of them is, as we have seen before, is we're just using runCatching and throwing
13:41an exception.
13:42Let's have a look at that example.
13:43So I have here a fetch book fail, and in this fetch book fail, all of the attempts will
13:48fail.
13:49And the idea is that we are able to catch it and just work around with that result.
13:53So we can see here that we call raceN just the same way as before.
13:56We throw an exception for the two coroutines that we are launching concurrently, and we
14:00do a merge, but then it will fail, and that means that we will get a result type at the
14:05end.
14:06It is type-safe.
14:07It is type-safe, but it's not really the most effective way to deal with exceptions, meaning
14:11that we are going to find in our logs at least 10 failed results.
14:15And that is because, as we have seen before, that we are calling this function 10 times.
14:19But probably the best way to deal with failed results isn't using exceptions.
14:24It's probably using either, which is one of these fundamental functional programming concepts.
14:29Let's try that in the next example, 3.3.
14:31This will be the last one, and it's called fetch book either fail.
14:35And let's have a look at how it is implemented at this point.
14:38So fetch book either fail will do exactly the same as before, except that now, instead
14:44of us throwing an exception, we are using either.
14:47And that means that we have, again, the raise scope, and that means that we can raise an
14:51error whenever that occurs.
14:53This makes more sense because then we can get a left or a right, depending on how the
14:57result of our operation will be.
14:59In this case, we are going to get 10 left because it is specifically programmed to raise
15:0310 failed of type string, of type string.
15:07Let's now run this program in bulk and examine the logs one by one and see what happens and
15:12compare it to what we have said before.
15:15So in the first example, we got a book with an ID and with a name.
15:18We see that it is in shelf.
15:20It has an ISDN number.
15:21It doesn't have a library associated.
15:22The first example, if you remember, it was the example about creating a book with parse
15:26in.
15:27We can see that it took one second in spite of getting the name, taking one second and
15:31getting the ISDN number, taking another second.
15:34But these two running in parallel take one second.
15:37If we look at example 1.2, we don't get anything.
15:39And the reason for that is that if you remember, we were using a run cat, and it was this example
15:44over here, where we see that we tried to create a book, but we run a run catching wrapped
15:48around it.
15:49We tried to print to the console how many seconds it took to execute, but that never
15:52happens because of course we throw an exception in between.
15:55And so we get an on failure.
15:56We could have printed the exception to the console, but that doesn't seem to be an efficient
16:01way to deal with errors at this point.
16:03And so we move on to the next example, to the next example, which is example 1.3, where
16:07we use parser to accumulate the results.
16:10In the previous example, we know that we have shortcutted our execution using exception
16:14throwing.
16:15But in this example, we are using race for that.
16:17So let's have a look once more at the try to create either book.
16:20We can see that we've got our either with our race scope, and then we just raise a problem.
16:25The moment that occurs, we immediately shortcut the execution of our application so that it
16:30returns a left with this value right over here, which is the first error that it finds,
16:36which is cannot get name.
16:38And now we move on to Parma.
16:40And in Parma, if we remember correctly, this was an example about getting all of the books
16:44that are associated with one particular book.
16:46And so using the ID of that book, we would be able to retrieve all of the other books.
16:51This retrieval would take one second per associated book, but we launched it using Parma, which
16:56starts all the different coroutines synchronously, and we'll join them together in a list at
17:02the end.
17:03And so here we expect successful result, successful result, and that is exactly what we have over
17:07here.
17:08So if we use Parma directly, we get all of the different books that we needed.
17:12And so we got a nice list with all of these simulated associated books.
17:16But now we think about how do we handle exceptions in this situation?
17:20How do we handle errors?
17:21And that is our second example.
17:23And the second example was a Parma fail.
17:25In the Parma fail example, we went straight into using either because we know already
17:30from the first example that throwing an exception is not really what we want to do.
17:33And so we try to use either to fix the situation.
17:38However, doing this means that the moment we get the first raised, the first moment
17:43that we raise fail in our execution means that the execution will short circuit to the
17:49end, and we will just get a left, which is exactly what we get here.
17:53And that is one left that we cannot get the name of one particular book.
17:58But it doesn't say anything about what happened to the other books because the execution was
18:01short circuited, and then we don't get anything else, and then we don't get anything else.
18:06So to fix that problem, what we decided to do was to use a cumulative way of retrieving
18:11all of these errors.
18:12And for that, if I remember correctly, we were going to use a ParMap or Accumulate.
18:18And by doing that, we can get everything together.
18:20But as we have said before, this is going to accumulate successes and fails.
18:26However, in this case, we only get fails because, of course, I'm telling it to fail all five
18:30different associated books that we have.
18:32And that means that we get five different cannot get named error messages in our logs.
18:38One thing that we can do before we continue to the other example is launch this whole
18:41execution once again.
18:43But in this case, we're going to just put here a small if to simulate random books that
18:47fail and hopefully we get lucky in the next execution.
18:50So we say if it which is the ID of the associated book is an even number, only then we raise
18:57a fail.
18:59So if we run it like this, it means that all the even numbers will raise a fail.
19:02Let's run it again.
19:04Let's run it again.
19:05So we can see in example 2.3 that we get only two fails.
19:11What this means is that because we've got one fail, thanks to the non-empty list type,
19:17we only get two fails, but the successful case is not shown in the logs.
19:21But that doesn't matter to us because at the moment that one error occurs, we just want
19:26to know about the fails and we don't care about successes because we want all of these
19:31attempts to be successful together.
19:34The next example is the final example of the series, example 3, where we were trying to
19:38get a book from two different libraries.
19:41The idea was to get the book out of the quickest of the coroutines that we sent in parallel.
19:48So we can see in the first result that we get the book that we want.
19:51One we get from the library in Ollau and we get the other one from the library in Gouda.
19:56And this is because we have called this process two times.
20:00But the way we have implemented it this way, but the way we have implemented it this way
20:04doesn't really account for any kind of problems that can occur along the line.
20:08And if we have problems, then we need to be able to have a good error handling capability.
20:13And in the first case that we tried to do that, if you remember, we decided to use run
20:17catching and then provide error handling by checking the result.
20:21And let's have a look at that.
20:22Now, once again, we essentially would wrap race and the merge function that we call later
20:27in a run catching function.
20:29And then that one would give us the result.
20:31And then we would see if there would be an error or not.
20:34This probably is not the best way to do the error handling.
20:36And so we moved on to use either and using either gives us this result, which probably
20:42looks much better.
20:43And in this case, we actually get the book from the same library in Ollau by calling
20:48the fetch book either fail.
20:50And in this case, we actually went to two different libraries in the two cases that
20:54we call the, in the two cases that we call the fetch book either fail function.
20:58And so in the first case, the successful coroutine got the book from Ollau, coroutine got the
21:02book from Ollau, and the other one got the book from, from Gouda.
21:07The only problem with this is that both of them failed.
21:10And so we've got two lefts.
21:12One says major fail Ollau, and the other one says major fail Gouda.
21:17If we compare this result with the result that we get on example 3.2, the difference
21:22is that on example 3.2, we need to check if the result is a failure or not.
21:29And in the last one, we get that out of the box with, and in the third example, we get
21:34that out of the box by using either.
21:47As a short disclaimer, I'd like to mention that I'm not associated or affiliated with
22:08any of the brands eventually shown, displayed, or mentioned in this video.

Recommended