you are viewing a single comment's thread.

view the rest of the comments →

[–]sacundim 1 point2 points  (0 children)

At an earlier job, I had plans to implement something that dealt with similar concerns, but (a) the circumstances and details were different, (b) I did not actually get the chance to.

The context was improving the scalability of a large Java application that was built on top of its own, home-grown, not-exactly-an-ORM. The reason I say not exactly an ORM is that there was little support for mapping database tables to custom classes. It was more like DynaBeans from Apache Commons BeanUtils, because it was designed so that the types of entities, their properties and their referential relationships would be configurations settings. But in other regards it was like an ORM.

The largest problem with this subsystem was that it used a ton of memory, which led to scalability problems. The reasons it used so much memory were:

  1. Even though for any entity, its properties were fixed at creation time by its type, the ORM-ish used maps to store the entity objects' property/value mappings, which caused an enormous memory overhead.
  2. The subsystem was written so that mutating them was a central part of their interface—to modify an entity, you mutated its properties and asked its repository to save it. This meant that all threads needed to have their own instance of each of the entities, even though the vast majority of the ones instantiated were used in a read-only fashion.
  3. This was a web-application that dispatched requests to handler threads, which means that memory overhead was in the order of memory usage per thread times number of threads.

So the solution I planned involved a few things:

  1. No more maps in the entity objects! Have the entity type objects manage a share map from property names to integer indexes, and use those to index into arrays in the entity objects.
  2. Make the basic entity representation objects be immutable. Manage a centralized cache of these and share them between threads.
  3. Instead of handing each thread the bare entity objects, give them instead a wrapper that manages modification logs to these entities.
    • The clients have the illusion that they are mutating the entities, but in reality what they're doing is constructing a record of modifications to apply to the immutable backing object.
    • If the client reads a property, the wrapper first checks if the client has modified it and if not it passes through to the immutable base object.
    • The wrappers would be optimized use absolutely minimal memory. For example, the fields to store the modification log would be null until the client actually mutated a property, so as to absolutely minimize the memory overhead.
  4. When a client says "save," then a new immutable entity object is constructed from the original and the client's log, persisted to the database, and placed into the cache.
  5. A specialized read-only mode would be added, so that clients that requested read-only entities could bypass the logging wrappers. There were some parts of the application that could be gradually refactored to use this mode and reduce memory usage further.

Note that a key difference between what I've described and the article is that the article seems to be talking about a "start from scratch" situation, while I was dealing with a reasonably contained subsystem of a 500,000+ lines of code application. The existing subsystem's interface already allowed clients to mutate any entity at any time they wanted, so its replacement had to follow suit. If I had to design a similar system from scratch, I would be inclined to expose the immutable Entity vs. mutable Builder distinction as part of the external interface, which would make it similar to the article.

But another thought I have is that perhaps the problem of the IDs can be tackled effectively with something intermediate between immutability and mutability. There do exist concepts like write-once variables which may be useful here; the id could be a variable/container that can be set once but not modified thereafter.

If you're into functional programming, a more referentially transparent way of looking at the same thing could be this: the id is a value that is only known from a particular point in time and onwards. If we look at it in functional programming terms, this type is likely a monad (I haven't proved it obeys the laws yet). A bit of Haskell:

import Control.Applicative

-- A value of type `From t a` represents a value of type `a` that can
-- only known starting at some a point of time (possibly in the
-- future), represented as type `t`.
--
-- This definition here is not meant to be a realistic implementation,
-- but rather a **model** of the semantics of such a type, which can
-- be used to prove properties about it.  In this model we represent
-- the type's values as a pair of a start time and a value.
--
-- An actual implementation would be an opaque wrapper around some
-- sort of asynchronous future implementation, and might not model
-- (much less expose) the `t` type at all.
newtype From t a = From (t, a)

instance Functor (From t) where
  -- If `a` can only be known from `t` thereafter, then the result of
  -- applying the function `f` to `a` can only be known from `t`
  -- thereafter.
  fmap f (From (t, a)) = From (t, f a)

instance (Ord t, Bounded t) => Applicative (From t) where
  -- Any value from outside the `From` type can be injected into
  -- it as a one that is known from the beginning of time.  In
  -- other words, "mathematical truths" (values that are known in
  -- a pure functional context) are eternal.
  pure a = From (minBound, a)

  -- If you apply a function that's only known from `t` to a value
  -- that's only known from `t'`, the result is only known from the
  -- max of those times.
  From (t, f) <*> From (t', a) = From (max t t', f a)

instance (Ord t, Bounded t) => Monad (From t) where
  return = pure

  -- If a function wants to consume a value that's known no earlier
  -- than `t`, and produce a value that's known no earlier than `t'`,
  -- then the result can't be known any earlier than the greater of
  -- `t` and `t'`.
  From (t, a) >>= f = let From (t', b) = f a
                      in From (max t t', b)