Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite #33

Merged
merged 158 commits into from
Sep 5, 2013
Merged

Rewrite #33

merged 158 commits into from
Sep 5, 2013

Conversation

fangpenlin
Copy link
Contributor

Restart from a solid ground up

Basic models are built (core business logic and RESTful API are not implemented yet), still not in functional stage. However, we have 97% testing coverage currently. I hope I can keep this through and eventually deliver with 100% testing coverage.

Name                        Stmts   Miss  Cover   Missing
---------------------------------------------------------
billy                           0      0   100%
billy.models                   19      9    53%   19-36
billy.models.company           37      0   100%
billy.models.customer          36      0   100%
billy.models.plan              48      0   100%
billy.models.subscription      45      1    98%   89
billy.models.tables            81      1    99%   35
billy.models.transaction       42      0   100%
billy.utils                     0      0   100%
billy.utils.generic            23      0   100%
---------------------------------------------------------
TOTAL                         331     11    97%
----------------------------------------------------------------------
Ran 39 tests in 1.282s

OK

To install and run testing

virtualenv --no-site-packages env
source env/bin/activate
python setup.py develop
python setup.py nosetests

Only merge this branch back when it is good to go.

"""


class SubscriptionModel(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@victorlin what's your thinking behind suffixing all the models with Model (e.g. SubscriptionModel) and then also including the type in the method name?

This seems very verbose:

from billy.models import SubscriptionModel

subscription = SubscriptionModel.create_subscription(...)    
subscription = SubscriptionModel.get_subscription_by_guid(xxx)
subscription.update_subscription(...)

To me, calling the model Subscription and the methods get and update respectively seems more concise and it doesn't detract from readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjallday I like to provide expressive naming in most cases, however, yeah, you're right, this is a overkill here. In certain model context, this cannot help readability afterall. I will correct them later.

But for Model suffixing, I think this is fine. The whole model here is different from what it used to be. I try to isolate business logic from SQLAlchemy object and operations. I don't want to expose too much SQLAlchemy details as when one day we like to switch NoSQL or something like that, it would be difficult to get rid of SQLAlchemy everywhere in the project, as I explained in #24. Also, there is table class named Subscription, I don't want them to be confusing. And from the big picture, it would be a MVC structure, the naming might look like this

[Subscription (table)] <---- [SubscriptionModel (business logic)] <---- [API web controller]

So, I will keep Model suffixing. Until I make this system workable, if you don't like naming style, I can make some change here.

@mahmoudimus
Copy link
Contributor

@victorlin going to address one of your comments here:

You wrote -

I got reasons to pass only GUID. Passing SQLALchemy table class instance around is error-prone in certain situations. For example, when you are dealing with multi-threading, passing object instance around causes problem. (emphasis added)

Since you're using a scoped_session and a sessionmaker, you shouldn't have to worry about multi-threading issues.

def process(records):
for record in records:
with transaction.manager:
record.num += 1
session.add(record)

pool = threading_pool()
for thread in pool:
records = get_records(100)
thread.run(process, records)

This is a really inefficient way of doing bulk updates or insertions. If you want to do a bulk-update, using threading is typically a really poor way of doing it. There are constructs in SQLAlchemy that allow you to very easily do bulk updates or insertions. In one call, using one transaction.

Let's say, we are processing transactions and we like to speed it up by using multi-threading or multi-processor. The code we wrote above looks fine, but it is not. You cannot create SQLALchemy object in one thread and pass it to another (exception will be raised). There is some thread-local stuff Session object is dealing with (sessions are bound to local threads). So, in this case, I prefer to make model only accept GUID argument rather than object.

There's other ways to do this if you want to pass a SQLAlchemy mapped object between threads. Simply, tell the ORM to stop tracking and instrumenting this object while you're passing it. Use Session.expunge()

Threads in Python are typically used for I/O heavy applications, so in that case, why not just pass the model, then do a Session.merge() and then do a Session.refresh().

Passing in objects is a very nice way to work with relationships. Not using them really limits the power of SQLAlchemy.

@@ -14,6 +14,7 @@ def create_tables():
"""
Creates the tables if they dont exists
"""
print '#'*10, 'Base.metadata', Base.metadata
Base.metadata.create_all(DB_ENGINE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using alembic for our database migrations.

@fangpenlin
Copy link
Contributor Author

@mahmoudimus

What I mean is not actually doing updates or insertions with threading. But for more complex business logic. For example, currently, we have yield_transactions method of SubscriptionModel. It is for yielding transactions from subscriptions. If we have a tons of subscription to process, it would be slow to do it in single thread. This kind of logic cannot be done with plain SQL expressions like bulk updates or insertions (it may could be, but might be too complex, or that needs to be optimized later).

I know there is a way I can leverage session to deals with cross thread issue. However, I prefer to keep things simple and stupid (KISS). The Session.refresh() will actually get the record from database by its primary key, just like what you do by passing GUID. So, in this case, for simplicity and consistency, l prefer passing primary keys. However, if passing object is more comfortable for youguys, I will refactory the model to do it the way later (When we reach the stable stage).

if transaction_type not in self.TYPE_ALL:
raise ValueError('Invalid transaction_type {}'.format(transaction_type))
transaction = tables.Transaction(
guid='TX' + make_guid(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern should be moved to a base model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we have duplicated code there. However, I will keep them there for a while. I would like to see what are those common elements shared by all modes, and extract them to a base class or something later. Sometimes by doing OO design too early, it would be over engineering or modeling it wrong. A little deferred refactorying would be helpful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use this same pattern. I can share it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@victorlin - definitely agree with you regarding OO design too early making it over engineering or modeling it wrong, but we've been working on this for almost 3 years now. We've got a couple of patterns that we can share w/ you that we've really nailed down over time.

One flaw I see with your method is if you ever have to override any of the uuid creation schemes, it's very hard to do so (you don't expose a method anywhere to do this).

I'll fork your branch of billy and make a pull request so I can demonstrate what I'm talking about. I'll also add you to our internal Balanced project so you can see how advanced it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I am actually building billy with my experience and my practices. I have done things with this kind of structure and patterns for tons of projects. Although, I am doing this for years, I am still improving it gradually.

The reason we have so many discussions here is that we have very different development experience and practices, surely it would take some time to get used to. And yes it would be nice if I can see how you do in the backend systems. I am sure I can learn something from that and apply on billy.

But for now, I can do it with the best productivity in my favor manners (in this week, maybe). So, I will keep working, and in the mean time, I will try get familiar with your style, then gradually refactory and transform into Balanced style. With good testing coverage, we can always switch between different implementation approaches easily. Does it make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely :)

@mahmoudimus
Copy link
Contributor

Also, are you going to recurring payout support? This is probably the most important distinguishing feature for Billy. See #15.

@fangpenlin
Copy link
Contributor Author

@mahmoudimus

Also, are you going to recurring payout support? This is probably the most important distinguishing feature for Billy. See #15.

Yes, recurring payout is included in this initial development cycle.

@fangpenlin
Copy link
Contributor Author

The features I am going to implement in this cycle are posted in #25 as usage scenario form. If you see anything that is needed badly but not there, please added it in a reply.

@fangpenlin
Copy link
Contributor Author

Okay, for now, there are still details to take care, however, we have reached a rough workable version with 100% testing coverage.

Name                                        Stmts   Miss  Cover   Missing
-------------------------------------------------------------------------
billy                                          12      0   100%
billy.api                                       7      0   100%
billy.api.auth                                 36      0   100%
billy.api.company                               4      0   100%
billy.api.company.views                        25      0   100%
billy.api.customer                              4      0   100%
billy.api.customer.views                       27      0   100%
billy.api.plan                                  4      0   100%
billy.api.plan.views                           33      0   100%
billy.api.subscription                          4      0   100%
billy.api.subscription.views                   37      0   100%
billy.api.transaction                           3      0   100%
billy.api.transaction.views                    16      0   100%
billy.models                                   15      0   100%
billy.models.company                           42      0   100%
billy.models.customer                          36      0   100%
billy.models.plan                              50      0   100%
billy.models.processors                         0      0   100%
billy.models.processors.balanced_payments      63      0   100%
billy.models.processors.base                   10      0   100%
billy.models.schedule                          20      0   100%
billy.models.subscription                      97      0   100%
billy.models.tables                            91      0   100%
billy.models.transaction                       91      0   100%
billy.renderers                                31      0   100%
billy.request                                   7      0   100%
billy.scripts                                   0      0   100%
billy.scripts.initializedb                     19      0   100%
billy.scripts.process_transactions             32      0   100%
billy.utils                                     0      0   100%
billy.utils.generic                            26      0   100%
-------------------------------------------------------------------------
TOTAL                                         842      0   100%
----------------------------------------------------------------------
Ran 136 tests in 7.081s

OK

To run the test

virtualenv --no-site-packages env
source env/bin/activate
python setup.py develop
pip install -r test_requirements.txt
python setup.py nosetests
# or you prefer 
nosetests

Create tables database

initialize_billy_db development.ini

Start the server

pserve development.ini --reload

Create a company

# create company
curl http://127.0.0.1:6543/v1/companies/ -X POST \
    -d "processor_key=YOUR_BALANCED_API_KEY"

result

{
    "guid": "CPPEfEKswV4eywR67He2ZkFu", 
    "api_key": "DCwfL7zJi48EesUCLSiyemCuxxdSiB7T1Bha9icTBN4d", 
    "created_at": "2013-08-23T06:39:04.427220", 
    "updated_at": "2013-08-23T06:39:04.427232"
}

Create a customer

# create customer
curl http://127.0.0.1:6543/v1/customers/ \
    -u DCwfL7zJi48EesUCLSiyemCuxxdSiB7T1Bha9icTBN4d: \
    -d "external_id=/v1/customers/AC1jqOF9TocQXGIXjuMVrpMu"

result

{
    "guid": "CUJn4Us9HqQmkEazeKDTsoYo", 
    "company_guid": "CPPEfEKswV4eywR67He2ZkFu", 
    "external_id": "/v1/customers/AC1jqOF9TocQXGIXjuMVrpMu", 
    "created_at": "2013-08-23T08:18:16.782940", 
    "updated_at": "2013-08-23T08:18:16.782950"
}

Create a plan

# create plan
curl http://127.0.0.1:6543/v1/plans/ \
    -u DCwfL7zJi48EesUCLSiyemCuxxdSiB7T1Bha9icTBN4d: \
    -d "plan_type=charge" \
    -d "amount=55.66" \
    -d "frequency=daily"

result

{
    "guid": "PLWZH4N9mB5wTLwEDTSgaPpf",
    "company_guid": "CPPEfEKswV4eywR67He2ZkFu", 
    "interval": 1, 
    "plan_type": "charge", 
    "amount": "55.66", 
    "frequency": "daily", 
    "created_at": "2013-08-23T08:20:56.820769", 
    "updated_at": "2013-08-23T08:20:56.820785"
}

Create a subscription

# create subscription
curl http://127.0.0.1:6543/v1/subscriptions/ \
    -u DCwfL7zJi48EesUCLSiyemCuxxdSiB7T1Bha9icTBN4d: \
    -d "customer_guid=CUJn4Us9HqQmkEazeKDTsoYo" \
    -d "plan_guid=PLWZH4N9mB5wTLwEDTSgaPpf"

result

{
    "guid": "SUBSHs7KijcmEmuqUFPEkNiP",
    "customer_guid": "CUJn4Us9HqQmkEazeKDTsoYo", 
    "plan_guid": "PLWZH4N9mB5wTLwEDTSgaPpf", 
    "period": 0, 
    "canceled": false, 
    "amount": "None", 
    "started_at": "2013-08-23T08:23:46.534374", 
    "next_transaction_at": "2013-08-23T08:23:46.534374", 
    "created_at": "2013-08-23T08:23:46.536058", 
    "updated_at": "2013-08-23T08:23:46.536065"
}

Process transactions

We have a subscription in the database, and it should yield a transaction right away. To process them, here you call

process_billy_tx development.ini

A crontab setting or similar scheduler can be used to process all transactions periodically.

What's next?

There are still some design decisions to make, such as should we allow modifying subscription, should we process transaction immediately when a subscription is added, I will think about them. I may create issues for them, then we can talk about it and see what kind of design is better.

Although the testing coverage is 100%, doesn't mean it is bug-free nor perfect, I am sure there are many edge cases aren't tested.

And for testing, we have unit tests and functional tests, they are all on SQLite. I will create a Vagrant environment and writes some integration tests there with PostgreSQL.

In the mean time, I may do some refactory, but the priority here are still the tests. With good testing coverage (include edge cases), we can move faster.

@mjallday
Copy link
Contributor

@victorlin you are amazing :)

@fangpenlin
Copy link
Contributor Author

@mjallday
Thank you :)

Well, I think this branch is good to merge back now. It is stable enough and also functional. I will create issues for remaining task later.

(thread local is used in Balanced-Python)
@mjallday
Copy link
Contributor

mjallday commented Sep 5, 2013

this pull-request is big and old. can we merge it so we don't have to look at 10k line pull requests ever again?

mahmoudimus added a commit that referenced this pull request Sep 5, 2013
@mahmoudimus mahmoudimus merged commit 95e52e4 into balanced:master Sep 5, 2013
@mahmoudimus
Copy link
Contributor

this looks great +1

@fangpenlin fangpenlin deleted the rewrite branch September 5, 2013 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants