Multi-User using Core Data?

  • I am looking for examples/ideas/tips for how to make a Core Data
    application multi-user. The idea would be for a common SQLite
    database to be accessed by the application from several different
    computers. Is this even possible with Core Data today? Searches of
    the documentation and Goggling have not produced anything helpful
    yet. Thanks for any pointers.

    ******************
    J. Scott Anderson
  • On Jun 9, 2005, at 8:59 AM, J. Scott Anderson wrote:

    > I am looking for examples/ideas/tips for how to make a Core Data
    > application multi-user. The idea would be for a common SQLite
    > database to be accessed by the application from several different
    > computers. Is this even possible with Core Data today?
    >
    In what sense "multi-user"?
    If you use the SQLite store, then you can benefit from non-atomic
    writes -- that is, a save does not overwrite the whole file -- which
    in principle allows multiple users to edit the same store
    simultaneously.  Core Data uses optimistic locking that can prevent
    one user overwriting modifications made by another user, but there is
    no "distributed notification" that a value has been changed "under" a
    user (you find out at save: time -- see also NSManagedObjectContext's
    detectConflictsForObject:).  You (obviously) also do not get database
    login, row- or table-level locking, or any other related features you
    typically associate with a client-server database.

    mmalc
  • Apple obviously made a conscious decision not to support remote
    databases with Core Data - they already had an architecture in place
    to do it (EOF), so it clearly could have been done had they really
    wanted to.  I have no reason if their reason not to include that
    support was technical, political, or what, but that being said, there
    are a couple of options give use a single data store for applications
    on separate machines.

    1) You could make the SQLite data file location a preference value,
    and set it to a network drive location.
    2) You could write a small, headless server application that would
    vend the data to you client applications using Distributed Objects.
    3) Create a subclass of NSPersistentStoreCoordinator that uses a
    remote database. This would be no mean feat, but you would likely
    make many friends by doing it =)

    On Jun 9, 2005, at 11:59 AM, J. Scott Anderson wrote:

    > I am looking for examples/ideas/tips for how to make a Core Data
    > application multi-user. The idea would be for a common SQLite
    > database to be accessed by the application from several different
    > computers. Is this even possible with Core Data today? Searches of
    > the documentation and Goggling have not produced anything helpful
    > yet. Thanks for any pointers.
  • > 2) You could write a small, headless server application that would
    > vend the data to you client applications using Distributed Objects.

    I just asked a similar question... and this is the approach I've been
    thinking about. I wonder what object(s) should be vended. The
    PersistentStoreCoordinator, the ManagedObjectContext, or the
    ManagedObjects themselves.

    If I send an object to a client when edits it, do those changes get sent
    properly to the server? What about undo, is there only one undo manager
    per ManagedObjectContext?

    > Core Data uses optimistic locking that can prevent
    > one user overwriting modifications made by another user, but there is
    > no "distributed notification" that a value has been changed "under" a
    > user (you find out at save: time -- see also NSManagedObjectContext's
    > detectConflictsForObject:).  You (obviously) also do not get database
    > login, row- or table-level locking, or any other related features you
    > typically associate with a client-server database.

    Is there a way to implement this notification and locking?

    -Justin
  • On 9 Jun 2005, at 21:50, Jeff LaMarche wrote:

    > Apple obviously made a conscious decision not to support remote
    > databases with Core Data - they already had an architecture in
    > place to do it (EOF), so it clearly could have been done had they
    > really wanted to.  I have no reason if their reason not to include
    > that support was technical, political, or what, but that being
    > said, there are a couple of options give use a single data store
    > for applications on separate machines.

    I think that the main reason for not doing this is that CoreData is
    essentially free but you have to pay for the Enterprise Objects
    framework, so they felt it was necessary to "cripple" the freeware to
    a specification that is useful for desktop applications but left the
    corporates with deep pockets needing to buy EOF.  This seems a
    perfectly reasonable business decision, even if it's a little
    annoying for those of us who want to run enterprise grade
    applications but don't have the cash!

    > 1) You could make the SQLite data file location a preference value,
    > and set it to a network drive location.

    This is fine until you have two applications accessing the same data
    at the same time at which point the memory cache and the file will
    get woefully out of sync and "bad things" will happen.

    > 2) You could write a small, headless server application that would
    > vend the data to you client applications using Distributed Objects.

    This has many merits, though I've no idea how well things like KVO
    work on distributed objects.

    > 3) Create a subclass of NSPersistentStoreCoordinator that uses a
    > remote database. This would be no mean feat, but you would likely
    > make many friends by doing it =)

    Actually, as I noted before (http://www.cocoabuilder.com/archive/
    message/2005/5/18/136233) what you need to do is build a few new
    subclasses of the NSSQLAdapter, NSSQLStatement and related classes.
    It's clear from examining the guts of CoreData that it is perfectly
    capable of working on other types of database back end.  You would
    need to arrange a suitable set of triggers and monitors on any tables
    that are having their content cached but this is standard practice in
    multiple-access database programming.  I now more or less understand
    how to build this but I don't really have the time or the database
    experience to do this properly.  If someone out in CocoaLand with
    some DB experience and lots of free time wants to drop me a line I'd
    be happy to tell them what I know.

        Nicko
  • On Jun 10, 2005, at 1:55 AM, Nicko van Someren wrote:

    >> Apple obviously made a conscious decision not to support remote
    >> databases with Core Data - they already had an architecture in
    >> place to do it (EOF), so it clearly could have been done had they
    >> really wanted to.  I have no reason if their reason not to include
    >> that support was technical, political, or what, but that being
    >> said, there are a couple of options give use a single data store
    >> for applications on separate machines.
    > I think that the main reason for not doing this is that CoreData is
    > essentially free but you have to pay for the Enterprise Objects
    > framework, so they felt it was necessary to "cripple" the freeware
    > to a specification that is useful for desktop applications but left
    > the corporates with deep pockets needing to buy EOF.

    Core Data is not a "crippled" EOF.  Its goal, and how it contrasts
    with EOF, is made clear in the FAQ:
        <http://developer.apple.com/documentation/Cocoa/Conceptual/
    CoreData/Articles/cdFAQ.html#//apple_ref/doc/uid/TP40001802-244739
    >

    Also note: WebObjects is bundled for free with the Developer Tools...

    >> 1) You could make the SQLite data file location a preference
    >> value, and set it to a network drive location.
    > This is fine until you have two applications accessing the same
    > data at the same time at which point the memory cache and the file
    > will get woefully out of sync and "bad things" will happen.
    >
    This ("bad things" will happen) is not the case.  Just like EOF, Core
    Data is designed to properly detect and deal with situations in which
    the persistent store is modified by another application, and there
    are well-specified patterns to follow if this occurs.

    mmalc
  • On 10 Jun 2005, at 10:23, mmalcolm crawford wrote:
    >
    > On Jun 10, 2005, at 1:55 AM, Nicko van Someren wrote:
    >
    >>> Apple obviously made a conscious decision not to support remote
    >>> databases with Core Data - they already had an architecture in
    >>> place to do it (EOF), so it clearly could have been done had they
    >>> really wanted to.  I have no reason if their reason not to
    >>> include that support was technical, political, or what, but that
    >>> being said, there are a couple of options give use a single data
    >>> store for applications on separate machines.
    >>>
    >> I think that the main reason for not doing this is that CoreData
    >> is essentially free but you have to pay for the Enterprise Objects
    >> framework, so they felt it was necessary to "cripple" the freeware
    >> to a specification that is useful for desktop applications but
    >> left the corporates with deep pockets needing to buy EOF.
    >
    > Core Data is not a "crippled" EOF.  Its goal, and how it contrasts
    > with EOF, is made clear in the FAQ:
    > <http://developer.apple.com/documentation/Cocoa/Conceptual/
    > CoreData/Articles/cdFAQ.html#//apple_ref/doc/uid/TP40001802-244739>

    I appreciate that one is not a crippled copy of the other.  What I
    was meaning is that the SQL database functionality of one is a subset
    of the other, e.g. a crippled specification.  CoreData adds a bunch
    of extra features too; fetched properties, local stores and store
    aggregation being key, but the remote, shared database server support
    is missing.

    > Also note: WebObjects is bundled for free with the Developer Tools...

    Really?  WO is available as a download for paid-up commercial
    developers through ADC Select and Premier membership but I don't see
    any sign of it in the free developer tools.

    >>> 1) You could make the SQLite data file location a preference
    >>> value, and set it to a network drive location.
    >>>
    >> This is fine until you have two applications accessing the same
    >> data at the same time at which point the memory cache and the file
    >> will get woefully out of sync and "bad things" will happen.
    > This ("bad things" will happen) is not the case.  Just like EOF,
    > Core Data is designed to properly detect and deal with situations
    > in which the persistent store is modified by another application,
    > and there are well-specified patterns to follow if this occurs.

    Really?  On the page of the link you gave above, under the heading
    Change Management it says:

        "There is an important behavioral difference between EOF and
    Core Data with respect to change propagation. In Core Data, peer
    managed object contexts are not "kept in sync" in the same was as are
    editing contexts in EOF. Given two managed object contexts connected
    to the same persistent store coordinator, and with the "same" managed
    object in both contexts, if you modify one of the managed objects
    then save, the other is not re-faulted (changes are not propagated
    from one context to another). If you modify then save the other
    managed object, then (at least if you use the default merge policy)
    you will get an optimistic locking failure."

    Clearly then there is no change propagation even if we are talking
    through the same Persistent Store Coordinator, let alone if we are
    accessing the same file from two different applications on two
    different machines over a network.  If we were using two application
    on the same machine perhaps we could watch the database files (using
    something like Uli's UKKQueue http://www.zathras.de/angelweb/
    sourcecode.htm#UKKQueue ) and call refreshObject:mergeChanges: on
    anything we care about, but as far as I know file change notification
    does not work reliable over network mounts.

    The section in the Core Data Programming Guide on Change Management
    <http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/
    Articles/cdChangeManagement.html
    > makes no mention whatsoever
    regarding multiple applications accessing the same source (on the
    same machine or different ones), though it makes repeated remarks
    about the same application accessing the same data through multiple
    Managed Object Contexts (which all talk to the same Persistent Store
    Coordinator).

    It would be very useful if Apple could make a clear statement (a
    Technical Note perhaps) about how to have multiple applications
    accessing the same data store through Core Data, and in particular
    how to do this with the applications on different machines.  If it is
    genuinely the case that Apple did not deliberately dumb down the
    specification of Core Data then it would be useful to know how to
    achieve the same effect as WO/EOF through Core Data.

        Nicko
  • On Jun 10, 2005, at 7:24 AM, Nicko van Someren wrote:

    >> Also note: WebObjects is bundled for free with the Developer Tools...
    >>
    >
    > Really?  WO is available as a download for paid-up commercial
    > developers through ADC Select and Premier membership but I don't
    > see any sign of it in the free developer tools.
    >

      It's part of the XCode Tools download (I just noticed it as of
    2.1, but it may have been earlier??).
  • On Jun 10, 2005, at 4:24 AM, Nicko van Someren wrote:

    > On 10 Jun 2005, at 10:23, mmalcolm crawford wrote:
    >> On Jun 10, 2005, at 1:55 AM, Nicko van Someren wrote:
    > I appreciate that one is not a crippled copy of the other.

    It's not clear, then, why you wrote that one is a crippled version of
    the other.

    >> Also note: WebObjects is bundled for free with the Developer Tools...
    > Really?  WO is available as a download for paid-up commercial
    > developers through ADC Select and Premier membership but I don't
    > see any sign of it in the free developer tools.
    >
    Umm, yes, really.  WebObject 5.3 was announced at WWDC.  It is
    bundled free with the developer tools.  The deployment license is
    bundled with Mac OS X Server.

    >>> This is fine until you have two applications accessing the same
    >>> data at the same time at which point the memory cache and the
    >>> file will get woefully out of sync and "bad things" will happen.
    >> This ("bad things" will happen) is not the case.  Just like EOF,
    >> Core Data is designed to properly detect and deal with situations
    >> in which the persistent store is modified by another application,
    >> and there are well-specified patterns to follow if this occurs.
    > Really?

    Again, yes, really...

    > On the page of the link you gave above, under the heading Change
    > Management it says:
    > "There is an important behavioral difference between EOF and
    > Core Data with respect to change propagation. In Core Data, peer
    > managed object contexts are not "kept in sync" in the same was as
    > are editing contexts in EOF. Given two managed object contexts
    > connected to the same persistent store coordinator, and with the
    > "same" managed object in both contexts, if you modify one of the
    > managed objects then save, the other is not re-faulted (changes are
    > not propagated from one context to another). If you modify then
    > save the other managed object, then (at least if you use the
    > default merge policy) you will get an optimistic locking failure."
    >
    This represents a subtle but important change in the specified
    pattern of behavior -- it does not mean that something "bad" is
    happening.  You're confusing change propagation *within the stack*
    with change propagation between the database and your application...

    > Clearly then there is no change propagation even if we are talking
    > through the same Persistent Store Coordinator, let alone if we are
    > accessing the same file from two different applications on two
    > different machines over a network.
    >
    There is no change propagation in EOF either, *from the database to
    the access layer*.

    It's not clear to me that you understand the way EOF deals with
    optimistic locking failures.  Perhaps you could explain what it is
    you perceive to be the difference between the way EOF and Core Data
    deal with an external change to the persistent store?

    mmalc
  • On 10 Jun 2005, at 14:58, mmalcolm crawford wrote:
    > On Jun 10, 2005, at 4:24 AM, Nicko van Someren wrote:
    >> On 10 Jun 2005, at 10:23, mmalcolm crawford wrote:
    >>> On Jun 10, 2005, at 1:55 AM, Nicko van Someren wrote:
    >> I appreciate that one is not a crippled copy of the other.
    >
    > It's not clear, then, why you wrote that one is a crippled version
    > of the other.

    I didn't.  In the same email that points out that the internals of
    CoreData are neatly structures to support other sorts of SQL database
    I wrote that "... felt it was necessary to "cripple" the freeware
    [CoreData] to a specification that is useful for desktop
    applications ...".  I concede that this might be misinterpreted and
    perhaps I should have phrased it better but all I was saying was that
    CoreData does not deliver as much functionality as it is clearly
    designed to be able to deliver, e.g. it is crippled, which is at odds
    with EOF.  Even though the underlying structure supports addition of
    other sorts of SQL database Apple have kept the API at that level
    closed and indeed have taken the trouble of setting classes such as
    NSSQLAdaptor as non-exported in the shared library so that it is
    difficult to subclass them.

    >>> Also note: WebObjects is bundled for free with the Developer
    >>> Tools...
    >>>
    >> Really?  WO is available as a download for paid-up commercial
    >> developers through ADC Select and Premier membership but I don't
    >> see any sign of it in the free developer tools.
    > Umm, yes, really.  WebObject 5.3 was announced at WWDC.  It is
    > bundled free with the developer tools.  The deployment license is
    > bundled with Mac OS X Server.

    OK, I wasn't at WWDC.  What confused me was the way that http://
    developer.apple.com/webobjects/ (still) says:

    "WebObjects and the Apple Developer Connection
    Order WebObjects 5.2 through the ADC Member Site and save over 25%.
    Premier members receive one (1) copy free, and may purchase
    additional copies for $399 US. Select members may purchase WebObjects
    5.2 for $399 US. Please allow 1 to 2 weeks for delivery, depending on
    location."

    It's nice to know that it's now freely available.  Thanks Apple.

    ...
    >> On the page of the link you gave above, under the heading Change
    >> Management it says:
    >> "There is an important behavioral difference between EOF and
    >> Core Data with respect to change propagation. In Core Data, peer
    >> managed object contexts are not "kept in sync" in the same was as
    >> are editing contexts in EOF. Given two managed object contexts
    >> connected to the same persistent store coordinator, and with the
    >> "same" managed object in both contexts, if you modify one of the
    >> managed objects then save, the other is not re-faulted (changes
    >> are not propagated from one context to another). If you modify
    >> then save the other managed object, then (at least if you use the
    >> default merge policy) you will get an optimistic locking failure."
    >>
    > This represents a subtle but important change in the specified
    > pattern of behavior -- it does not mean that something "bad" is
    > happening.  You're confusing change propagation *within the stack*
    > with change propagation between the database and your application...

    OK, perhaps I'm missing something but I was under the impression that
    the change propagation in EOF worked between instances of
    applications, via the database.

    >> Clearly then there is no change propagation even if we are talking
    >> through the same Persistent Store Coordinator, let alone if we are
    >> accessing the same file from two different applications on two
    >> different machines over a network.
    >>
    > There is no change propagation in EOF either, *from the database to
    > the access layer*.

    Aha.  That was my misapprehension.

    > It's not clear to me that you understand the way EOF deals with
    > optimistic locking failures.  Perhaps you could explain what it is
    > you perceive to be the difference between the way EOF and Core Data
    > deal with an external change to the persistent store?

    Well, my perception was that EOF allowed for triggers on external
    changes, which you tell me is not the case, so I guess the point is
    moot.

    Returning to the original topic of this thread, since you seem to
    know something about how this works, can you explain what CoreData
    does about implementing it's locking on the SQLite files with a view
    to understanding network sharing?  SQLite has Shared->Reserved-
    > Pending-> Exclusive locks and on Unix like machines these are
    typically implemented using Posix advisory locks.  Advisory locks
    have been buggy on NFS for years; if AFP reliably implements
    networked locking then things _should_ work if you simply point
    multiple programs on multiple machines at a shared copy of the file.

    That said, the authors of SQLite explicitly advise against using it
    in this way; see the "Situations where another RDBMS may work better"
    on http://www.sqlite.org/cvstrac/wiki?p=WhenToUseSqlite where it says
    "A good rule of thumb is that you should avoid using SQLite in
    situations where the same database will be accessed simultaneously
    from many computers over a network filesystem."  So we are still left
    without a good way to have multi-user access to Core Data files :-
    (  Perhaps one day Apple will open up the SQL abstraction layer that
    lurks within Core Data and either give us the RDBMS adaptors we need
    or allow us to write our own...

        Nicko
  • On Jun 10, 2005, at 11:17 AM, Nicko van Someren wrote:
    > I didn't.  In the same email that points out that the internals of
    > CoreData are neatly structures to support other sorts of SQL
    > database I wrote that "... felt it was necessary to "cripple" the
    > freeware [CoreData] to a specification that is useful for desktop
    > applications ...".  I concede that this might be misinterpreted and
    > perhaps I should have phrased it better but all I was saying was
    > that CoreData does not deliver as much functionality as it is
    > clearly designed to be able to deliver, e.g. it is crippled, which
    > is at odds with EOF.
    >
    "Cripple" implies that there is functionality that is present and
    complete, but which is deliberately turned off.  This is simply not
    the case.

    > Even though the underlying structure supports addition of other
    > sorts of SQL database Apple have kept the API at that level closed
    > and indeed have taken the trouble of setting classes such as
    > NSSQLAdaptor as non-exported in the shared library so that it is
    > difficult to subclass them.
    >
    There are many reasons why this may be the case, not least because
    the implementation may not be considered sufficiently well abstracted
    to allow subclassing, or because the API is not finalised.  There are
    many examples of functionality being restricted to private frameworks
    for precisely these reasons...

    I suspect that you misunderstand or do not fully appreciate what is
    involved here.  In many respects, the current Java implementation of
    EOF is made much easier because of JDBC.  EOF is able to make use of
    a single JDBC adaptor, and database developers provide custom drivers
    below that to support communication with their product.  It may be
    instructive to look at the documentation for WebObjects 4.5: <http://
    developer.apple.com/documentation/LegacyTechnologies/WebObjects/
    WebObjects_4.5/webobjects.html>
    In particular note:
        InformixEOAdaptor
        ODBCEOAdaptor
        OracleEOAdaptor
        SybaseEOAdaptor

    In principle, each database requires its own adaptor, client
    libraries etc.  In practice, it may be sufficient to implement an
    ODBC adaptor, but that still requires third party support...

    In summary, however, it's untrue to claim that Core Data is
    "crippled" -- there is a considerable amount of extra work that would
    be required to support additional persistent store types...

    > Returning to the original topic of this thread, since you seem to
    > know something about how this works, can you explain what CoreData
    > does about implementing it's locking on the SQLite files with a
    > view to understanding network sharing?  SQLite has Shared->Reserved-
    >> Pending->Exclusive locks and on Unix like machines these are
    > typically implemented using Posix advisory locks.  Advisory locks
    > have been buggy on NFS for years; if AFP reliably implements
    > networked locking then things _should_ work if you simply point
    > multiple programs on multiple machines at a shared copy of the file.
    >
    Bill would be better able to comment on the file-level locking issues
    here.  In general, however, Core Data (as EOF) uses optimistic
    locking to ensure that two users do not overwrite the same data.
    This does not rely on file (or row) locking...

    > Perhaps one day Apple will open up the SQL abstraction layer that
    > lurks within Core Data and either give us the RDBMS adaptors we
    > need or allow us to write our own...
    >
    Please submit enhancement requests.
    When doing so, it would be very useful if you could indicate, for
    example, how critical such functionality is to your product, how many
    users you believe you could bring to the platform if such
    functionality were provided, and the relative importance of such
    functionality -- e.g. given a choice between support for ordered
    relationships and database adaptors, which *one* would you choose?

    Note: The preceding example is purely an *example*.  This should not
    in any way be taken to suggest that this choice would be one that
    would be made in practice, nor should it be taken to suggest that
    Apple might provide either or both features in the future.  It is
    intended to highlight the fact that there are clearly resource
    constraints, and that in any given release cycle there is a limit to
    the amount of new functionality that can be implemented.  I'm sure
    the team would love to implement all the features that everyone is
    asking for, but this is simply not possible.  Feedback from the
    developer community is very useful in determining priorities...

    mmalc
    To emphasise: Speaking only for himself.
  • On Jun 11, 2005, at 7:27 PM, mmalcolm crawford wrote:
    >> Returning to the original topic of this thread, since you seem to
    >> know something about how this works, can you explain what CoreData
    >> does about implementing it's locking on the SQLite files with a
    >> view to understanding network sharing?  SQLite has Shared-
    >>> Reserved->Pending->Exclusive locks and on Unix like machines
    >> these are typically implemented using Posix advisory locks.
    >> Advisory locks have been buggy on NFS for years; if AFP reliably
    >> implements networked locking then things _should_ work if you
    >> simply point multiple programs on multiple machines at a shared
    >> copy of the file.
    > Bill would be better able to comment on the file-level locking
    > issues here.  In general, however, Core Data (as EOF) uses
    > optimistic locking to ensure that two users do not overwrite the
    > same data.  This does not rely on file (or row) locking...

    Sure.  Off the cuff answer below.  First, from a pure SQLite
    perspective:

    Of note, all locking in SQLite is performed on the entire database.

    On filesystems that support POSIX advisory locks, SQLite uses the
    same exact locking mechanisms as it would if you were to build the
    stock SQLite.

    For AFP, SQLite uses byte range locks to simulate a locking behavior
    that is as close to advisory locks as you can get without having true
    advisory locks.  The only difference is that you might see a
    situation where what should be a shared lock appears as an exclusive
    to another process or connection.  Given that your code should be
    dealing with this situation anyway -- what if some random process
    should grab the database and do something to tables you have no
    control over? -- this should not pose a huge problem.

    For NFS, SQLite will test to make sure that advisory locks really
    work.  If they do, they are used.  If not, .lock files are used.

    For SMB (Samba), SQLite will use flock() style locks.  Since there
    is no way to upgrade a shared flock() lock to an exclusive flock()
    lock without offering a window of time within which another app can
    effectively 'steal' the lock, flock() based locks will only support
    exclusive locks.

    Note that NFS does not fall back to flock() if advisory locks do not
    work.  Testing revealed that this was the safe way to go.

    Note that mixing file systems when accessing a single SQLite database
    file is a really, really bad idea.  The alternative was to basically
    go with .lock files on all filesystems.

    All SQLite unit tests passed except those that should fail when
    shared locks are not available.

    The API for the additional locking support is at the end of
    sqlite3.h.  The underlying changes will be posted back to the SQLite
    development community as time permits.  Now that WWDC is over, time
    is much more permissive...

    ---

    What does this mean for Core Data?

    Core Data does not keep SQLite connections open for any longer than
    is necessary to perform a particular operation.  While any given
    transaction may be relatively involved-- may use a number of SQL
    statements in its internal implementation -- Core Data will have
    closed all transactions before returning control to your code (or the
    main-event-loop, in the case of pure bindings based apps).

    In other words, if you are doing a complex, large scale, fetch in one
    process using Core Data, it will block other processes and will
    sometimes do so more than others, depending on filesystem.  However,
    this situation should occur  fairly rarely.

    b.bum
previous month june 2005 next month
MTWTFSS
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      
Go to today
MindNode
MindNode offered a free license !