Transaction errors

Topics: General, Writing modules
Dec 2, 2011 at 2:58 PM

Hi,

I'm trying to use the module Luxit Redirect that allows one to specify an old-url / new-url value pair to deal with changing URLs in a site. No one seems to have mentioned this problem before on that module. I've tried contacting the owner, but no luck so far.

But I keep getting Transaction errors (Transaction aborted, operation not valid for the state of the transaction, ...). 

What it does is that it tries to match the route with an existing alias, by calling Match(...) (in an IRouteConstraint, ISingletonDependancy class) which relies on a service class (IDependancy) that uses an IRepository to check the existance of the alias in the db.

(hope that made sense)

Anyways, also I'm relatively new to Orchard dev, I can't see why this would trigger such errors... 

Any idea what might be the problem ? I can of course provide additional info ...

Thanks,

Dec 2, 2011 at 4:21 PM

Hi,

This is something I've also wanted to solve for a while, and I finally have a working solution.

I don't know if you've heard talk about the Alias module (it should be part of Orchard 1.4) but it provides a routing engine of aliasing paths to routes. I found it very easy to then create a RedirectionController, then alias old paths to a call to the Redirect action with the new path.

The Transaction errors you're seeing are a problem where the Transaction Scope hasn't yet been created during routing. Because of this all the current routing mechanisms (and Alias also) hold an in-memory singleton map of all the paths.

Alias is at: http://orchardalias.codeplex.com/

Although if you want it to work in 1.3 you'll need this fork: http://orchardalias.codeplex.com/SourceControl/network/Forks/randompete/AutorouteDevelopment

Dec 2, 2011 at 4:35 PM

Thanks I'll look into that on monday.

When you say I have to fork the autoroute development ... I'm not sure what you're talking about

We're developing modules to customize orchard. We've actually just started but are working with the Web version. Is it bad practice ? Should we be working on a clone of the source instead ?

Thanks

Dec 2, 2011 at 4:38 PM

I didn't say you had to fork autoroute :) Well firstly Alias and Autoroute are separate projects (although Autoroute will use Alias's services). But all you have to do is clone my fork, you don't need to create one. I've just been making it compatible with Orchard 1.3.

It's advised to have a full source clone for any serious module development, yes.

Coordinator
Dec 2, 2011 at 5:11 PM
To get back to the initial problem, there is another module, Sébastien's rewrite rules module, that works today. We are using it on the orchardproject.net site.

Sent from my TI-99/4A

From: arnaudm
Sent: 12/2/2011 7:58 AM
To: Bertrand Le Roy
Subject: Transaction errors [orchard:281616]

From: arnaudm

Hi,

I'm trying to use the module Luxit Redirect that allows one to specify an old-url / new-url value pair to deal with changing URLs in a site. No one seems to have mentioned this problem before on that module. I've tried contacting the owner, but no luck so far.

But I keep getting Transaction errors (Transaction aborted, operation not valid for the state of the transaction, ...).

What it does is that it tries to match the route with an existing alias, by calling Match(...) (in an IRouteConstraint, ISingletonDependancy class) which relies on a service class (IDependancy) that uses an IRepository to check the existance of the alias in the db.

(hope that made sense)

Anyways, also I'm relatively new to Orchard dev, I can't see why this would trigger such errors...

Any idea what might be the problem ? I can of course provide additional info ...

Thanks,

Dec 2, 2011 at 5:27 PM

Thanks,

Can't seem to find it in the gallery though could you give me some pointers ?

Coordinator
Dec 2, 2011 at 5:54 PM

http://gallery.orchardproject.net/List/Search?packageType=Modules&searchTerm=rewrite+rules

First result

Dec 2, 2011 at 6:17 PM

my mistake I typed rule ... I'll look into it ! thanks

Dec 5, 2011 at 9:34 AM
I've tried adding the Alias Module as you told me but it won't compile :
No overload for method 'Fetch' takes 0 arguments AliasStorage.cs 83



Dec 5, 2011 at 2:59 PM

You have the wrong version, you need the fork that I linked to (http://orchardalias.codeplex.com/SourceControl/network/Forks/randompete/AutorouteDevelopment)

The Mercurial Clone URL is: https://hg01.codeplex.com/forks/randompete/autoroutedevelopment

Dec 6, 2011 at 10:20 AM
Ok ! I now have the correct version and it works, that is I managed to activate the features

But, I'm wondering how it works ... Could you provide me some examples ?

Thanks
Dec 6, 2011 at 12:04 PM

Enable the "Alias UI" feature. This gives you an admin menu where you can add aliases, e.g. /old-content-path -> /MyModule/Redirect?path=/new-content-path

Dec 6, 2011 at 12:10 PM
Yes that's what i did but i don't understand the synthax of the second
url. Here's a scenario: i changed a page's url from /aaa/page1 to
/bb/page1, what does the alias looks like? Or do you mean that i need
to develop a module that make redirections ? Thanks...
Dec 6, 2011 at 1:14 PM

At the moment I'm not sure whether a RedirectionController should be included in Alias itself, or in another module, which is why I haven't added one yet (I'll probably add one for the time being anyway).

The example I gave before is what the alias might then look like, it just depends how the controller/route is implemented.

Dec 6, 2011 at 3:21 PM
Ok, so I've created a simple redirection module that works like you described:

RedirectController, action To, parameter targetUrl does a redirection
It works, because when I type in localhost/Redirect?targetUrl=/some/page I get to localhost/some/page
and my route is defined as follow:

new RouteDescriptor {
                      Priority = 5,
                      Route = new Route(
                               "Redirect",
                               new RouteValueDictionary {
                                   {"area", "ADP.Redirect"},
                                   {"controller", "Redirect"},
                                   {"action", "To"}
                               },
                               new RouteValueDictionary(),
                               new RouteValueDictionary {
                                  {"area", "ADP.Redirect"}
                               },
                      new MvcRouteHandler())
                    }
(ADP.Redirect is the name of my module)
But when I try to define an alias (say my old url is /another-page and it's now /pages/another-page) I define:
alias : /another-page
route path : Redirect?targetUrl=/pages/another-page
and add the alias (no error)
but when I go to localhost/another-page I still get a 404 (instead of a redirect)

What am I doing wrong ? 

I must admit I'm still a bit lost when it comes to routing ...

Thanks !
Dec 6, 2011 at 3:25 PM

Can you have a look in the Orchard_Alias_ActionRecord and Orchard_Alias_AliasRecord tables and tell me what's in there?

Dec 6, 2011 at 3:30 PM
In Action Record :
5, ADP.Redirect, NULL, NULL
In Alias Record :
5, /another-page, 5, <v action-="To" controller-="Redirect" targetUrl="/parking/another-page" />
Dec 6, 2011 at 4:39 PM

Interesting ... it's failed to populate action/controller in the ActionRecord. I'll investigate.

Dec 6, 2011 at 4:47 PM

I'm wondering if it was a string comparison problem. I've just pushed an update which adds a StringComparison.CultureInvariant, can you update to that and see if you can now create aliases correctly? (You'll probably want to clear those tables)

Dec 6, 2011 at 4:48 PM
Oh ok, I'll take a look at it too (I'm starting to understand what's going on under the hood ... and getting the hang of it !)

Next step for me is to find a way to know when a page's url is being updated so that I can automatically create an alias to handle existing links to that url.

Any idea about how I could implement that would be really appreciated ...
Thanks
Coordinator
Dec 6, 2011 at 4:51 PM

Want to do some "Redirects", seriously, use the Rewrite Rules modules, I don't think the alias is done and suitable for that. And it will be lighter.

Dec 6, 2011 at 4:56 PM
Thanks for the advice but I've tried it and the problem is I need to keep the new url not the old one
All I've achieved with the rewrite module was that when I setup something like
/old-url -> /new-url, when going to localhost/old-url I would get the content but the url would remain old-url; What I need is to have the new-url

If it's doable with the rewrite module I'm interested ...
Dec 6, 2011 at 5:01 PM

Alias is feature complete as far as I'm aware, this is the only bug I know of (and I can't reproduce it locally). But yeah, for a few small redirects, Rewrite will be way simpler. If you want automatic redirection for renamed pages, probably best to wait for Autoroute because everything will be changing then and it'll be much easier, or even included.

Coordinator
Dec 6, 2011 at 5:01 PM

YES ;)

just add a [R] at the end of the rule to generate a Redirect, as opposed to a Transfer which is the default (an internal rewrite)

You can also set [R=301] for a permanent redirect 

Dec 6, 2011 at 5:09 PM
OK :)
so /old-url /new-url [R] makes an actual redirect and
/old-url /new-url [R=301] make a permanent redirect ?

@randompete : I just can't afford to wait unfortunately ... unless autoroute is due 2 weeks from now ?

And as far as my general problem is concerned, how should I address it ?

To sum things up, what I need is as follow:
one creates a page, make links to it in several other pages, and later, changes the url for that page, how can I create a redirect so that the links to that page don't become dead ?
I naively thought that having a redirect module + some sort of interceptor to know when a url changes would be enough ...


Coordinator
Dec 6, 2011 at 5:13 PM

Your scenario suits very well with Alias, when it's done, and I won't blame Pete even if it takes him months to do it, he doesn't even get rewards for what he does ;)
Rewrite rules is a manual solution until alias gets done for this precise usage. 

Dec 6, 2011 at 7:33 PM

It might actually be possible to have something working that soon. For starters, it's probably not hard simply to use an IContentHandler to catch urls changing and add the Alias redirects. But Autoroute is also coming along nicely, and I want to have something working pretty soon. Should be making an initial push to codeplex tonight, anyway.

Dec 7, 2011 at 9:23 AM
Hi,
I've tried with your update but it doesn't work either (Action record still empty for controller & action), but good news is I think I figured it out:
in AliasStorage Set method, you check for "area" and "area-", but not for "controller-" nor "action-" and that's what I get.

So I added those to the comparison and now the actionrecord looks fine :
6, ADP.Redirect, Redirect, To
and AliasRecord :
6 /another-page 6 <v targetUrl="/parking/another-page" />

But, when calling localhot/another-page I still end up on a 404

Although I'd like to help debug this, I still have to understand how and when the routes are examined/created ... Could you tell me more about how it works ?

Also, in the meantime, I'm going to try and use an IContentHandler to catch the url update and create the appropriate alias.

I'm really looking forward to solve this !
thanks
Dec 7, 2011 at 1:52 PM

Thanks, I really appreciate the help trying to debug it.

Part of the problem is most of the code was written by someone else, all I've done is make it work with Orchard 1.3, and fix a couple of bugs that I found. I'm still not 100% sure why some route values sometimes have a "-" appended. But you're right the code where that is checked - one bug I already fixed was a similar thing happening with "area-" and that's when I added in the line to check for "area-", which cleared things up for me so I assumed controller and action weren't a problem. Perhaps it's because you defined a Route, something I didn't do in testing.

I just tried aliasing an RSS feed (since RSS defines a similar route) and sure enough I got a 404. However this cleared up after a minute or so and the Url started working, perhaps you could check again? I think it's because there's a background process to update the alias map, so things only change after a minute.

Also, I need to properly look into what those hyphens are for, I have an idea of course, but it is a bit strange.

What would also be helpful is if you post the full code for your redirect controller and route, then I'll test that directly. We can also perhaps provide that in Alias itself, there seemed to be a general consensus that it'd be a good feature (you might then need to agree to a contributor agreement!)

Dec 7, 2011 at 2:02 PM
Ok, I figured that the mapping was delayed ... I'll try, but I'm fairly sure that I already tried this.

I'll be working on Orchard for work for a few months, so if I have the opportunity, I'll be glad to contribute in any way I can :)

But for now I'm stuck with my problem, do you think you could point me in the right direction ?

What I figure was a good thing to do was:
- using a filter, get the url of a page when I hit edit (still have to figure out how, I'm guessing I should be able to do that with contentmanager and looking at the filtercontext.route.values)
- and again with the filter, catch the save or publish action, compare the new url with the previous one and if necessary, create a redirect (using alias or rewrite rule)

The thing is, I find it really hard to figure out where to look in the code ... and which interface to use for what purpose
Thanks

Dec 7, 2011 at 2:07 PM
and as far as my redirect so-called module, it's plain and simple:
1 RedirectController :

 public class RedirectController : Controller {
 
        public ActionResult To(string targetUrl)
        {
            return new RedirectResult(targetUrl);
        }
    }



and in the Route file :


Route = new Route(
                "Redirect",
                new RouteValueDictionary {
                        {"area", "ADP.Redirect"},
                        {"controller", "Redirect"},
                        {"action", "To"}
                 },
                 new RouteValueDictionary(),
                 new RouteValueDictionary {
                        {"area", "ADP.Redirect"}
                 },
                new MvcRouteHandler())

Couldn't be easier (it is my first module!)

Any chance you could connect on IRC to chat ? If you have a minute ... Thanks!
Dec 7, 2011 at 2:29 PM

Something to check - is your module definitely called "ADP.Redirect"? (It should be correct, since your route works normally, but worth double checking)

I can IRC; where?

Dec 7, 2011 at 2:31 PM
It's called that way indeed ...
I'm on freenode, #orchard
Dec 8, 2011 at 12:33 AM
Edited Dec 8, 2011 at 12:41 AM
arnaudm wrote:

Although I'd like to help debug this, I still have to understand how and when the routes are examined/created ... Could you tell me more about how it works ?

I don't think I properly replied to this.

Basically, Alias does some pretty complex stuff under the hood. When you give it a path, it passes that through a chunk of the MVC pipeline and performs some custom parsing, to determine the actual route value dictionary lying underneath that route.

For example, you tell it "/rss?id=1" - and it will actually map that to { area="Feeds" controller="Feed" action="Index" format="rss" id="1" }. Finally it stores that dictionary as XML on the AliasRecord. Actually, area/controller/action are *supposed* to be removed, because they are stored on the ActionRecord. In your case, two of them aren't getting removed.

But it doesn't end there. Alias also has to take all that data and turn it into an in-memory map of the aliases which it can traverse in both directions - either taking a Url and returning a RouteValueDictionary, or vice-versa: taking a RouteValueDictionary and telling you what its alias is. This is where things get really tricky because it needs to be able to traverse the map in both directions, and extremely quickly - the reverse lookup could be performed ridiculous numbers of times in a single page render. Every time you ask MVC for a route url this happens, because we need to know if that Url is aliased and return the corresponding. So for instance when I say @Html.ActionLink("Item","Display",new{area="Contents",id=2}), I will actually get back the aliased Url, not just Contents/Item/Display/2.

So the list of aliases is massaged into a hierarchy of nodes that's pretty hard to understand in itself, but makes the lookups extremely quick. If you debug you'll also notice that these lookups execute many times, even for a single lookup. This is because there is a route instance per Area. Each area then maintains its own copy of the map to perform quick lookups on just that area.

Finally there's a background process that populates the maps with updated aliases. It's not very efficient, and aliases aren't added as you go. This is something I need to look at (there was already a todo there).

The classes to look in for all this happening are AliasHolder, AliasMap, AliasStorage, AliasUpdater, AliasRoute and DefaultAliasService. These are all in the Implementation folder. Also Routes is relevant because it creates the route-per-area.

Dec 8, 2011 at 5:05 AM

arnaudm: if you pull the new changes to Alias, I've rewritten things a bit. Aliases now take effect immediately so there should be no more waiting for the background task. Actually the background task is disabled completely now because it shouldn't be needed. The memory map now also uses ConcurrentDictionary so there's no more locking; theoretically it should perform way better both under seriously heavy load and with large numbers of content items. We will see with some profiling :)

I've also commented a lot of the code as I figured out more of what is happening. The hyphens are still a bit of a quandary, but I have an idea what's going on, and it looks to me like they are necessary in certain places; which might be why things have stopped working for you after the changes you made. Can I just check that you have both Pulled all the changes, as well as Updated to the latest revision? (Sorry if you're already familiar with Mercurial, I'm just making sure!)

Dec 8, 2011 at 9:49 AM
Hi,

Thanks for the explanations and the hard work you put into making this work ...
Unfortunately, although it is now working properly (as in, I create a new alias and it works right away), I saw that removing an alias had yet to be implemented. But my use cases require me to allow that as well.

So I think I might consider the option to use Rewrite module and create actual redirects. Which won't prevent me from moving back to Alias once it's completed.

Use cases are :

1 - existing page; many links to this page's url exist. User changes Url (from A to B)
--> create a redirect to keep links alive (redirect A to B)
2 - same page, url changes again (from B to C)
--> create another redirect (redirect B to C)
--> alter previous redirect (redirect A to B becomes A to C)

This should work ...
My only consideration is how well will that perform under heavy load, especially in the case where there are many redirects defined.
Dec 8, 2011 at 1:49 PM

Good to hear it works; hopefully that bug has gone away!

I didn't quite get around to being able to remove aliases; it was very late and it's just slightly tricky! Actually it should only be a few lines of code to make that work (but also consider: if a page has been deleted, you'll still get a 404 whether the alias exists or not, so it kind of doesn't matter...) And in the case where you're creating redirects yourself, it doesn't matter anyway.

Regarding Autoroute; I don't know if you're aware, I posted a working demo last night. See http://orchard.codeplex.com/discussions/274916. Now, somewhere between Alias and Autoroute I want to put in an event that gets triggered when an Alias changes, and then plug in a redirect. I'd like to have it in Alias but it might actually be cleaner to do it in Autoroute and leave Alias very simple. Just need to make that decision.

Performance of Alias should be very good now. Lookups both ways are keyed in such a way to make them near instant, and the background process no longer exists. Although, what sort of numbers of redirects are you likely to eventually have? Would be good to do some profiling at this point on Alias with extremely large numbers and multiple threads and see how it handles.