"Semaphore" that keeps only latest triggering instance of a macro running

I have been looking here in the forum, and in the wiki, for a Semaphore Lock resembling approach that keeps only the latest instance of macro running. I.e. if a macro is triggered again while running, it should cancel any already running instances of the macro, but keeps the newest triggering instance running. Always giving priority to the newest instance, is another way of putting it.

I was not sure if I should post this in Questions & Suggestions, Tips & Tutorials or here in the Macro Library. As I've ended up with a seemingly well functioning group of macros I choose the latter, but even though I share my approach here as kind of a neatly packed up macro group, I am by no means confident that this is the best way of achieving the desired behaviour, so I'd also love any kind of feedback or further ideas!

By the way sorry for my extremely longwinded post here, I am not good with technical explanations like this, but as I find the approach I share here to be both useful, and kind of cool, I wanted to try to explain/document it as best as I could.


ă…¤
I might be mistaken, but I at least have not been able to find any way of simply achieving the behaviour explained in the first paragraph above. So instead I've worked out an approach that in use resembles the Semaphore Lock a great deal:
ă…¤

This "Semaphore Action Group" lets the user set a unique Semaphore Name in the Execute Subroutine Action. In this group an instance variable is also set to the currently executing instance UUID.

Macro Image, of the Subroutine

This Subroutine creates a global "Semaphore Lock Variable" from the Semaphore Name chosen by the user (this variable is prefixed with "SemaphoreLockVariable_" to prevent clashing with any already existing global variables). This Semaphore Lock Variable will hold both the current and last executing instance UUID – if triggered more than one time) – this Subroutine will cancel the last executing instance, but keeping the latest executing instance running. The Subroutine also executes an asynchronously run Submacro that waits until the latest executing instance of the caller have finished running; before it clears the Semaphore Lock Variable (setting it to the empty string).

Macro Image, of the Submacro

ă…¤
I am sure something like this could be achieved simpler. And I know for a fact that it could have been achieved without running three levels deep. But I choose to set it up in this way so that it would make the user experience more closely resembling that of an actual Semaphore Lock action. I choose to use a Subroutine for the second level, over a Submacro, for this very reason (as it have less clutter on the caller end, where the semaphore lock name must be entered). And since Subroutines cannot be ran asynchronously I then in turn had to also call a Submacro, that asynchronously waits for the caller to finish before clearing the global variable.
ă…¤

There is no problem using a copy of the same "Semaphore Action Group" (calling the same Subroutine and Submacro) on different macros, as long as the Semaphore Name is set uniquely in each of the macros you want to be able to run simultaneously. Setting different macros with the same semaphore name is also possible. But if another macro with the same semaphore name is triggered it will cancel the already running macro midway. This is of course the very nature of this approach, but I just thought I'd mention it, as a macro left in a half-finished state can possibly lead to some unwanted behaviour.
ă…¤


Here is the macro group containing the Level 1 "Semaphore action group", the Level 2 Subroutine, and the Level 3 Submacro:

Semaphore- Keep Only Latest Instance Running Macros.kmmacros (166 KB)
(v11.0.2)

Macro Group Image

ă…¤


DEMONSTRATION:

Demonstration Macro Images

The macro group also includes two macros demonstrating this alternative variant of a semaphore lock.
Each of these macros are set up to run for 10 second, counting from when it was last triggered. By triggering these demonstration macros numerous times, less than 10 second apart, you will be able to observe how only the latest instance is kept running (especially if you check the KM menu bar menu to see which macros are currently running (the asynchronously ran Submacro(s), waiting to clear the Semaphore Lock Variable(s), will of course also show up in this list)).

Each of the demonstration macros are set up with Semaphore Names unique from each other, demonstrating that they can both run simultaneously. If you set them both to the same name you can observe that only the last triggered macro is kept running. But beware that the demonstration macros I've set up each rely on a global counter variable only cleared at the very end of the macros, so if set up with the same semaphore name, one of the macros will not run to it's end, and this global counting macro must then be cleared manually (e.g. by running the last canceled macro once more to completion).
ă…¤


PS: As a tip, if you like this shared approach, I recommend saving the action group from the L1 macro as a "favourite action" (right click, Add to Favourites), making it easy to implement it like you would any other semaphore action.


Edit: Fixed a small mistake in one of the demonstration macros

2 Likes

Wow Alexander…

thanks for sharing this interesting approach. I’ve been trying a lot to find a way like you did it with this approach and was always failing.

Many many thanks for posting this … if this is also fully backwards compatible with KM 9 & 10 the better and the more I will love and use it.

Since I am currently not able to test anything out… is it right that the SubMacro will wait until the very last instance of Macros has finished (let’s say I want to have for example 10 or more Macros running each with a unique Semaphore) ?

Thanks again for this great work!

Greetings from Germany

Tobias

1 Like

Thank you so much for you appreciative words!

As there’s at the very least a couple of “set variable array”-actions in there, I sadly do not believe it will be directly backwards compatible from KM11. However these are both set through tokenized variables in the variable name field, and I do not know/remember if this was possible before KM11, so maybe. Anyways as there’s almost always numerous other possible approaches to any given end goal within KM, I believe there’s probably nothing in there that can’t be worked around also for KM9/10!

I have not tested it with more than three simultaneously running macros, but I cannot see any reason why it shouldn’t work with more. The actual Semaphore Lock Action, present at the Submacro level, is set through the %TriggerValue% to the same Semaphore Name as set in the “Semaphore Action Group”. Each macro set with a unique semaphore name will therefore have it’s own running instance of the Submacro, waiting until each of the different caller macros have finished executing.

Perhaps a method other than a semaphore?

My first thought was to use the %ExecutingInstances% and %ExecutingInstanceName% token -- iterate through the list and cancel everything with a matching name (except this instance, obvs!). But that could be problematic because you can't guarantee the uniqueness of a macro name and you can't (AFAIK) get from instance to macro UUID.

But what you could do is maintain a dictionary of {UUID:instanceID} and have a subroutine, called at the start of any macro you wanted involved, that (pseudocode):

receives current macro's UUID and instanceID
for every entry with a matching UUID
   cancels specific macro instanceID
   removes that dictionary entry
end
adds current macro's UUID and instanceID to dictionary

Make sure that the cancel action doesn't abort the macro if the targeted instance is no longer running and then you don't even have to clean up your dictionary at the end of this instance's execution -- one orphaned entry per macro UUID will hardly break the memory bank!

Totally off the top of my head, and probably missing some features of your macros -- which I shall now deep-dive into (and shamelessly steal from!).

Have you got an example use case you could add to the beginning of your OP? That'd be a good way of getting across to people why they might want to do this.

1 Like

This seems over complicated.. or maybe I'm missing something.

All you need is two simple macros. One is a 'wrapper' macro, the other is your 'target' macro (run most recent)

  • In the target, save the instance ID when it starts in a variable
  • In the Wrapper cancel the macro that's running and run a new one.

Like this:
Target macro:
Only do the most recent version of this macro.kmmacros (2.0 KB)

Wrapper Macro:
Execute most recent.kmmacros (1.9 KB)

Proof of concept of this method -- disable the "Cancel" action, mash the Run button a few times, check the macro count in KM's menu item "Cancel", enable the "Cancel" action, hit Run again, and you should only find one remaining running instance (you can check the log to see it's the latest):

Only Latest Instance Demo.kmmacros (4.7 KB)

Image

Obviously more complicated than @johns excellent method, but because it's a group of actions you can save it as a Favourite then simply drop it in at the start of any macro that needs it and it'll just work.

2 Likes

I do like the idea of doing it in one go, instead of a wrapper!

If you only ever call the macro with the wrapper then you'll never have two instances running. If you do.. well.. something else might be broken right?

But if it's that mission critical. then you can just save the %ExecutingInstance% in a list and iterate in your wrapper.

We're definitely getting into "more than one way to skin a cat" territory here, so for future readers it's going to be a choose your own adventure. I prefer simplicity (i.e. copy/paste my solution) until I find myself re-writing the same pattern more than 4 or 5 times, then it's time to refactor. For me, I don't run across this need often enough to make it complex (like OP), but perhaps for some folks that's a better way to go.

Same with mine -- you only get multiple instances by turning off the "Cancel" action, and that's only to make it obvious that they are being cancelled. (Telling you to check that the menu shows one instance, you run the macro again and it then shows... one instance. That's a bit boring!)

The wrapper's a nice way of doing it for one macro, but how do you cope with a dozen? A wrapper for each, along with the resultant extra globals? If OP's going as far as making a subroutine we can assume that this isn't a one-off!