0

I am using quartz and nhibernate and ran into a problem. Normally I have all my nhibernate sessions close on finish of a web request but I have a scheduler that starts on application start and I need to pass in a nhibernate session that I think should never be closed.

I am unsure how to do that.

Ninject

 public class NhibernateSessionFactoryProvider : Provider<ISessionFactory>
    {   
        protected override ISessionFactory CreateInstance(IContext context)
        {
            var sessionFactory = new NhibernateSessionFactory();
            return sessionFactory.GetSessionFactory();
        }
    }

  public class NhibernateModule : NinjectModule
    {
        public override void Load()
        {
            Bind<ISessionFactory>().ToProvider<NhibernateSessionFactoryProvider>().InSingletonScope();
            Bind<ISession>().ToMethod(context => context.Kernel.Get<ISessionFactory>().OpenSession()).InRequestScope();
        }
    }

Global.aspx

  protected void Application_Start()
    {
        // Hook our DI stuff when application starts
        IKernel kernel = SetupDependencyInjection();

        // get the reminder service HERE IS WHERE THE PROBLEMS START
        IScheduledRemindersService scheduledRemindersService = kernel.Get<IScheduledRemindersService>();

        scheduledRemindersService.StartTaskRemindersSchedule();

        RegisterMaps.Register();

        AreaRegistration.RegisterAllAreas();

        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);


    }


    public IKernel SetupDependencyInjection()
    {
        IKernel kernel = CreateKernel();
        // Tell ASP.NET MVC 3 to use our Ninject DI Container
        DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel));

        return kernel;
    }

    protected IKernel CreateKernel()
    {
        var modules = new INinjectModule[]
                          {
                             new NhibernateModule(),
                             new ServiceModule(),
                             new RepoModule()
                          };

        return new StandardKernel(modules);
    }

// service that is causing me the problems. Ninject will bind reminderRepo and give it an nihbernate session.

private readonly IReminderRepo reminderRepo;
private readonly ISchedulerFactory schedulerFactory;

public ScheduledRemindersService(IReminderRepo reminderRepo)
{
    this.reminderRepo = reminderRepo;
    schedulerFactory = new StdSchedulerFactory();
}

public void StartTaskRemindersSchedule()
{

    IScheduler scheduler = schedulerFactory.GetScheduler();

    scheduler.Start();

    JobDetail jobDetail = new JobDetail("TaskRemindersJob",null,typeof(TaskReminderJob));
    jobDetail.JobDataMap["reminderRepo"] = reminderRepo;

    DateTime evenMinuteDate = TriggerUtils.GetEvenMinuteDate(DateTime.UtcNow);


    SimpleTrigger trigger = new SimpleTrigger("TaskRemindersTrigger", null,
                        DateTime.UtcNow,
                        null,
                        SimpleTrigger.RepeatIndefinitely,
                        TimeSpan.FromMinutes(1));

    scheduler.ScheduleJob(jobDetail, trigger);
}

So I need to pass in the reminderRepo into the job as I am doing above

jobDetail.JobDataMap["reminderRepo"] = reminderRepo;

It's the only way you can pass something into a job. Everytime the schedule gets executed a job is recreated and I am assuming it uses the same reminderRepo that I sent in.

My code in the service layer never gets executed again and of course the application start as well(unless I redeploy the site)

Job

 public class TaskReminderJob : IJob
    {


        public void Execute(JobExecutionContext context)
        {
            JobDataMap dataMap = context.JobDetail.JobDataMap;
            ReminderRepo reminderRepo = dataMap["reminderRepo"] as ReminderRepo;

            if (context.ScheduledFireTimeUtc.HasValue && context.NextFireTimeUtc.HasValue && reminderRepo != null)
            {
                DateTime start = context.ScheduledFireTimeUtc.Value;
                DateTime end = context.NextFireTimeUtc.Value;

                List<PersonalTaskReminder> personalTaskReminders = reminderRepo.GetPersonalTaskReminders(start, end);

                if (personalTaskReminders.Count > 0)
                {
                    reminderRepo.DeletePersonalTaskReminders(personalTaskReminders.Select(x => x.ReminderId).ToList());


                }

            }
        }

Reminder Repo. (When this repo gets instantiated a session should be given that will live till the end of the request)

  public class ReminderRepo : IReminderRepo
    {

        private readonly ISession session;

        public ReminderRepo(ISession session)
        {
            this.session = session;
        }

        public List<PersonalTaskReminder> GetPersonalTaskReminders(DateTime start, DateTime end)
        {
            List<PersonalTaskReminder> personalTaskReminders = session.Query<PersonalTaskReminder>().Where(x => x.DateToBeSent <= start && x.DateToBeSent <= end).ToList();
            return personalTaskReminders;
        }

        public void DeletePersonalTaskReminders(List<int> reminderId)
        {
            const string query = "DELETE FROM PersonalTaskReminder WHERE ReminderId IN (:reminderId)";
            session.CreateQuery(query).SetParameterList("reminderId", reminderId).ExecuteUpdate();
        }


        public void Commit()
        {
            using (ITransaction transaction = session.BeginTransaction())
            {
                transaction.Commit();
            }
        }


    }

So I need some way of keeping the session alive for my reminders. All my other sessions for all my other repos should be as I have it now. It's only this one that seems to need to live forever.

Edit

I tried to get a new session each time so I am passing the IsessionFactory around. Probably not 100% best but it was the only way I could figure out how to get some new sessions.

I however do not know if my session are being closed through ninject still since I am manually passing in the session now. I thinking now but cannot verify.

 **private readonly ISessionFactory sessionFactory;**
private readonly ISchedulerFactory schedulerFactory;

public ScheduledRemindersService(ISessionFactory sessionFactory)
{
    **this.sessionFactory = sessionFactory;**
    schedulerFactory = new StdSchedulerFactory();
}

public void StartTaskRemindersSchedule()
{

    IScheduler scheduler = schedulerFactory.GetScheduler();

    scheduler.Start();

    JobDetail jobDetail = new JobDetail("TaskRemindersJob",null,typeof(TaskReminderJob));
    **jobDetail.JobDataMap["reminderRepo"] = sessionFactory;**

    DateTime evenMinuteDate = TriggerUtils.GetEvenMinuteDate(DateTime.UtcNow);


    SimpleTrigger trigger = new SimpleTrigger("TaskRemindersTrigger", null,
                        DateTime.UtcNow,
                        null,
                        SimpleTrigger.RepeatIndefinitely,
                        TimeSpan.FromMinutes(1));

    scheduler.ScheduleJob(jobDetail, trigger);
}

So my global.aspx is the same but since ninject now sees that "ScheduledRemindersService" now takes in a nhibernate session factory it binds one for me that I can use.

I then pass it off to the job.

public void Execute(JobExecutionContext context)
{
    JobDataMap dataMap = context.JobDetail.JobDataMap;
    ISessionFactory sessionFactory = dataMap["reminderRepo"] as ISessionFactory;

    if (sessionFactory != null)
    {
        ISession openSession = sessionFactory.OpenSession();
        ReminderRepo reminderRepo = new ReminderRepo(openSession);
    }
}

I then pass it into my ReminderRepo so I am guessing it ignores the auto session binding from ninject but I am not 100% sure thus I am not sure if my sessions are being closed.

STW
  • 44,917
  • 17
  • 105
  • 161
chobo2
  • 83,322
  • 195
  • 530
  • 832

3 Answers3

7

Is there a reason the job can't just open up a new session every time it runs? Presumably it's running at periodic intervals and not forever ever.

Keeping stuff open forever is usually a sure-fire way to encounter weird behavior.

Hope this helps.

invertedlambda
  • 596
  • 4
  • 7
  • if I knew how I probably opt for this but what I have is the only way you can pass something in and the job gets recreated each time. So either in the void Execute(JobExecutionContext context) I would need to create a new session there but I don't know how to. – chobo2 May 16 '11 at 03:08
  • Ok I think I might have done what you said by making a new session however I have no clue if the session gets automatically closed or not and not sure how to test it. I think it is not but can't be sure. See edit – chobo2 May 16 '11 at 03:26
  • For testing purposes, you could wrap the injected session in an object of your own that implements the same interface. You could then delegate all the calls you care about to the wrapped object, but then log messages right beforehand to make sure that session close gets called correctly. (This is the Decorator Pattern). – invertedlambda May 17 '11 at 06:30
1

I think the approach to this is altogether wrong. The application scope of a web app is not the place to schedule events or try to persist "state" as it is still a website and the web server will not always have the app "started". This occurs after server restart and before app request, during app pool recycling and other misc cases like load balancing.

The best place to schedule something is either using a windows service or build a console app and then use windows scheduler to run it periodically. Any polling operations like that will cause major issues if you rely on application state.

I would also have serious concerns about the memory cost of letting a data context persist indefinitely even if you could count on it.

Check out building a console app - you'll find it's pretty easy even if you haven't done it before.

The other bonus is when you're sure of what you're doing, console apps can be switched to windows services with relative ease.

Gats
  • 3,452
  • 19
  • 20
  • yes console apps are nice and easy to make but I don't have the money to host my site on a dedicated server that I could run a console application or windows scheduler. I have to make due with that I got and that is a shared hosting. – chobo2 May 16 '11 at 03:09
  • Have you thought about using a scheduled operation on SQL server? You could use client script to check for the entries and delete when read, then just have an agent job that runs the updates. I think you're really going to find that it just doesn't work properly in App state. The objects instantiated in App_Start will just get nulled later. Stateful objects don't persist well when the a different process is in charge of garbage collection. – Gats May 16 '11 at 05:25
  • If you MUST do it in app state, then you're going to have to check for null state and/or open state no matter what you use as I am 100% sure you'll find the session null without the app necessarily having restarted at times. – Gats May 16 '11 at 05:26
0

I've developed a similar solution recently.
I chose to use a "windows service" to manage my schedules.
Anyway, I don't understand why you do something like this:

ISessionFactory sessionFactory = dataMap["reminderRepo"] as ISessionFactory;

Since I had the same problems I've decided to (only in this situation) get a SessionFactory from StructureMap (that's what I've used) and open a new session by myself.

This is my job:

public class ReminderScheduleJob : IStatefulJob
    {
        private readonly ILogger _Logger;
        private readonly ISessionFactory _SessionFactory;

        public ReminderScheduleJob()
        {
            this._Logger = ObjectFactory.GetInstance<ILogger>();
            this._SessionFactory = ObjectFactory.GetInstance<ISessionFactory>();
        }

        void IJob.Execute(JobExecutionContext context)
        {
            using (var session = this._SessionFactory.OpenSession())
            {
                using (var tx = session.BeginTransaction())
                {
                ...
                }
            }

        }
    }

UPDATE:

This is my nHibernate registry (see structureMap for more infos) which is called at the start-up of my app (asp.net or windows service):

public class NhibernateRegistry: Registry
{
    public NhibernateRegistry()
    {

        For<ISessionFactory>()
            .Singleton()
            .Use(new BpReminders.Data.NH.NHibernateSessionFactory(myConnectionString, schemaOperation).SessionFactory);

        For<IUnitOfWork>()
            .HybridHttpOrThreadLocalScoped()
            .Use<BpReminders.Data.NH.UnitOfWork>();

        For<ISession>()
            .HybridHttpOrThreadLocalScoped()
            .Use(o => ((BpReminders.Data.NH.UnitOfWork)o.GetInstance<IUnitOfWork>()).CurrentSession);
    }
}

I've used a unit of work but that doesn't make any difference for you.
I've defined another registry (structureMap) for Quartz.net cause I want (and that's what they say) it to be singleton:

public class QuartzRegistry : Registry
{
    public QuartzRegistry()
    {
        var properties = new NameValueCollection();

        // I set all the properties here cause I persist my jobs etc on a DB.

        For<ISchedulerFactory>()
            .Singleton()
            .Use(new StdSchedulerFactory(properties));

        For<IScheduler>()
            .Singleton()
            .Use(x => x.GetInstance<ISchedulerFactory>().GetScheduler());
    }
}

Now, in the global.asax I would start the scheduler asking StructureMap/ninject to resolve the IScheduler:

var scheduler = StructureMap.ObjectFactory.GetInstance<IScheduler>();
scheduler.Start();

I've seen you create a new scheduler in the ScheduledRemindersService:

public ScheduledRemindersService(IReminderRepo reminderRepo)
{
    this.reminderRepo = reminderRepo;
    schedulerFactory = new StdSchedulerFactory();
}

Since in my example the scheduler is already created and started as singleton you can ask ninject to have the instance ... and schedule your job.
In your job (TaskReminderJob) now you can ask the ObjectFactory to resolve the ISessionFactory (see the construct of my class ReminderScheduleJob). You can now open a new session and pass the session to your repository.
When everything is done you can dispose your session. Hope I've been clear enough.
As I told you I've got 2 layers. My web app is responsible to schedule the jobs and I've got a custom windows service (I haven't used the one available with Quartz.net) which is responsible to fetch the triggered events and do some actions.

LeftyX
  • 35,328
  • 21
  • 132
  • 193
  • I did that because that was the only way I could figure out how to get a new session. Could show me what your ObjectFactory looks like? Also is there something wrong with the way I did it? – chobo2 May 16 '11 at 15:54
  • @chobo2: I did exactly as you did. In my global.asax I've bootstrepped my DI.My SessionFactory is singleton and my session is HttpContextScoped. Anyway, I'll try to put some more infos in my previous answer. – LeftyX May 17 '11 at 08:48