Home Upgrade Search Memberlist Extras Hacker Tools Award Goals Help Wiki Contact

C# 

Reservation system, handling dates?

Long story short, I'm writing a reservation system for a warehouse, the warehouse has shelves all have the same size, so you can think of it like a 3D array. My current implementation uses a 3D bool array where false means the shelf is free, and true means it's taken, on start shelves data is loaded from DB into the manager class, all assigning of shelves is done using this class as 2 steps:
1- Check availability: user enters the # of storage spaces he wants, if they're available, he gets an ID for that transaction where he can confirm during a window of 10 minutes, after that the ID is invalidated and he'll have to check again.
2- Confirm: this is where the DB is updated and users now owns those spaces.
This implementation is good enough as a start to get a "working" API for others to use in development, however it sustains from serious business problems:
1- If someone reserves storage space with starting date after 2 months, the storage space shows as reserved immediately (due to bool being set to true)
2- If the warehouse is full or the user orders a # of storage spaces larger than currently available (but it will be available at the date the order is effective) it shows as can't be processed since currently the bool array doesn't have enough empty spaces.

Anyone have some solutions and maybe sample implementation for this?

I've already thought of querying DB on every order and building a "state" of the warehouse on everyday of the renting period and checking if there's available space, however this will look super ugly, harm the memory, put load on DB, and it'll be hard to track where each item is going since shelves may need to be changed during the period.
Have a cool signature? hit me up and I might use it.
How about posting your current implementation so we can give comments on it?


(Apr 30, 2018 08:38 AM)severos Wrote:  I've already thought of querying DB on every order and building a "state" of the warehouse on everyday of the renting period and checking if there's available space, however this will look super ugly, harm the memory, put load on DB

A query on a user-action is not 'bad' for the database. Databases are built for storing and retrieving large amounts of data. Imo this is fine.
(Apr 30, 2018 09:30 AM)Dalvik Wrote:  How about posting your current implementation so we can give comments on it?



A query on a user-action is not 'bad' for the database. Databases are built for storing and retrieving large amounts of data. Imo this is fine.

I'll post the code in couple hours soon as I'm home.
As for querying DB, it'll add delay specially that the manager class uses lock, also the main problem would be in building the state after the query, so if the warehouse has 1k shelves, then we're building 3D array with 1k elements per request... I don't think that's a friendly way of doing it right?
Have a cool signature? hit me up and I might use it.
(Apr 30, 2018 09:44 AM)severos Wrote:  I'll post the code in couple hours soon as I'm home.
As for querying DB, it'll add delay specially that the manager class uses lock, also the main problem would be in building the state after the query, so if the warehouse has 1k shelves, then we're building 3D array with 1k elements per request... I don't think that's a friendly way of doing it right?

I still have a hard time painting a picture of what you are doing. I will wait until you post your implementation before I make any further comment.
Here's the current implementation:

PHP Code

<?php 
public class SingleWarehouseStorageService
    
{

        private readonly 
object _lock;
        private readonly 
ApplicationDbContext _context;
        private readonly 
int _warehouseId;
        private readonly 
Config _config;

        private 
bool[,,] _usedStorage;
        private 
Dictionary<string, (Task task, List<(int x, int y, int z)>)> _tempTasks;

        public 
PriceSchema PriceSchema => _context.Warehouses.Where(o => o.Id == _warehouseId)
            .Include(
p => p.PriceSchema).First().PriceSchema;

        public 
SingleWarehouseStorageService(ApplicationDbContext context, Config config, int warehouseId)
        {
            
_config = config;
            
_lock = new object();
            
_context = context;
            
_warehouseId = warehouseId;
            
Refresh();

        }

        
async void Refresh()
        {
            while (
true)
            {
                
LoadFromDb();
                
await Task.Delay(new TimeSpan(0, _config.WarehouseCacheMinutes, 0));
            }
        }

        public 
void LoadFromDb()
        {
            var 
now = DateTime.UtcNow;
            var 
config = _context.Warehouses.Find(_warehouseId);
            var 
spaces = _context.Spaces.Where(s => s.EndDate > now);
            
spaces.Load();
            
lock (_lock)
            {
                
_tempTasks = new Dictionary<string, (Task task, List<(int x, int y, int z)> idx)>();
                
_usedStorage = new bool[config.MaxX, config.MaxY, config.MaxZ];
                foreach (var 
space in spaces)
                {
                    if (
_usedStorage[space.X, space.Y, space.Z])
                        throw new 
Exception("This should never happen, 2 items in same storage space...");
                    
_usedStorage[space.X, space.Y, space.Z] = true;
                }
            }
        }

        public 
string TempAllocate(int count)
        {
            
lock (_lock)
            {
                var 
testStorage = new bool[_usedStorage.GetLength(0), _usedStorage.GetLength(1),
                    
_usedStorage.GetLength(2)];
                var list = new List<(
int x, int y, int z)>(count);
                for (
int i = 0; i < testStorage.GetLength(0) && count > 0; i++)
                {
                    for (
int j = 0; j < testStorage.GetLength(1) && count > 0; j++)
                    {
                        for (
int k = 0; k < testStorage.GetLength(2) && count > 0; k++)
                        {
                            
testStorage[i, j, k] = _usedStorage[i, j, k];
                            if (
testStorage[i, j, k])
                                continue;
                            
testStorage[i, j, k] = true;
                            list.
Add((i, j, k));
                            
count--;
                        }
                    }
                }

                if (
count != 0)
                    return 
null;
                
_usedStorage = testStorage;
                var 
id = Guid.NewGuid().ToString();
                
_tempTasks.Add(id, (Task.Delay(new TimeSpan(0, _config.OrderTimeoutMinutes, 0)).ContinueWith(task => FreeTempAllocate(id)), list));
                return 
id;
            }
        }

        public 
void FreeTempAllocate(string id)
        {
            
lock (_lock)
            {
                if (!
_tempTasks.Remove(id, out var result))
                    return;
                foreach (var (
x, y, z) in result.Item2)
                {
                    
_usedStorage[x, y, z] = false;
                }
            }
        }

        public List<
StorageSpace> ConfirmAllocate(IEnumerable<ConfirmAllocationRequest> requests, int orderId)
        {
            var 
result = new List<StorageSpace>();
            
lock (_lock)
            {
                var 
total = 0d;
                var 
schema = PriceSchema;
                foreach (var 
request in requests)
                {
                    if (!
_tempTasks.Remove(request.Id, out var entry))
                        throw new 
Exception("Couldn't find id...");
                    
result.AddRange(entry.Item2.Select(location =>
                    {
                        
total += (schema.BaseCost + (request.EndDate - request.StartDate).Days * schema.DailyRate) *
                                 (
1 + schema.TaxPercent);
                        return 
_context.Spaces.Add(new StorageSpace
                        
{
                            
X = location.x,
                            
Y = location.y,
                            
Z = location.z,
                            
StartDate = request.StartDate,
                            
EndDate = request.EndDate,
                            
OrderId = orderId,
                            
WarehouseId = _warehouseId,
                            
PriceSchemaId = schema.Id
                        
}).Entity;
                    }).
ToList());
                }

                var 
order = _context.Orders.Single(o => o.Id == orderId);
                
order.Price = total;
                
_context.Orders.Update(order);
                
_context.SaveChanges();
            }

            return 
result;
        }
    } 
Have a cool signature? hit me up and I might use it.
(May 1, 2018 02:47 PM)Owl Wrote:  An immediate (but naive) solution would be to replace the 3D array as type DateTime, using null to represent a vacancy. You should really consider redesigning as a proper DB since you're implementing concurrency control and order tracking yourself.


I would table reserves separately and consult them when determining if an order can be accommodated. This way you don't face overbooking or false rejections.

I'm pretty open to any solution, the reason I'm handling concurrency my self is to reduce calls to DB and building the model on every request.
Have a cool signature? hit me up and I might use it.
You should use a database to persist data state. In memory is ephemeral. Is your data important enough that if the app throws an exception, the power goes out, that the reservation data doesn't need to be persisted?

You should profile your database before being concerned about database load. Regardless, you can eager load the entire DB to mem if you want and there wouldn't be any trips to the DB from read.

Refactor your 3D array into business classes/objects:

public class Warehouse
{
public ICollection<Shelf> Shelves {get;set;}
public int GetAvailableShelves(Reservation reservation) {// find available shelves}

}

public class Shelf
{
public int Row {get;set;}
public int Col {get;set;}
public Reservation {get;set;}
public Available => Reservation == null;

}

public class Reservation
{
public DateTime StartTime {get;set;}
public DateTime EndTime {get;set;}
public string UserId {get;set;}
}
(May 2, 2018 09:38 PM)ppaida Wrote:  You should use a database to persist data state. In memory is ephemeral. Is your data important enough that if the app throws an exception, the power goes out, that the reservation data doesn't need to be persisted?

You should profile your database before being concerned about database load. Regardless, you can eager load the entire DB to mem if you want and there wouldn't be any trips to the DB from read.

Refactor your 3D array into business classes/objects:

public class Warehouse
{
  public ICollection<Shelf> Shelves {get;set;}
  public int GetAvailableShelves(Reservation reservation) {// find available shelves}

}

public class Shelf
{
  public int Row {get;set;}
  public int Col {get;set;}
  public Reservation {get;set;}
  public Available => Reservation == null;

}

public class Reservation
{
public DateTime StartTime {get;set;}
public DateTime EndTime {get;set;}
public string UserId {get;set;}
}

Data is persistence in DB, I'm using memory to cache the result instead of querying DB over and over, the only data not being stored in a DB is checked but not confirmed, and i's not really a problem if this is deleted since client side can handle it by resending check.
The idea of refactoring DB have crossed my mind, but never found a good implementation for it, the implementation you provided will force me to create a shelf entry seeding, not that big problem, but it assumes that every shelf has 1 reservation, while in fact it has a history of reservations and only allowed a single active reservation.
Have a cool signature? hit me up and I might use it.
(May 2, 2018 10:56 PM)severos Wrote:  Data is persistence in DB, I'm using memory to cache the result instead of querying DB over and over, the only data not being stored in a DB is checked but not confirmed, and i's not really a problem if this is deleted since client side can handle it by resending check.
The idea of refactoring DB have crossed my mind, but never found a good implementation for it, the implementation you provided will force me to create a shelf entry seeding, not that big problem, but it assumes that every shelf has 1 reservation, while in fact it has a history of reservations and only allowed a single active reservation.

You could just as easily do:

public class Shelf
{
public int Row {get;set;}
public int Col {get;set;}
public ICollection<Reservation> Reservations {get;set;}
public bool IsReserved(DateTime start, DateTime end)
{
return Reservations.Any(//whatever your criteria is for being reserved);
}


}


Possibly Related Threads...
Thread / Author Replies Views Last Post
[Python] by Robin2Jan 13, 2019 06:27 AM 2 123 Jan 13, 2019 06:27 AM
Last Post: lockhead
[Python] by ApexLeadAug 16, 2018 06:25 AM 2 124 Aug 16, 2018 06:25 AM
Last Post: hash
[.NET] by PriorityNewMay 18, 2017 07:28 AM 4 135 May 18, 2017 07:28 AM
Last Post: Milch1