Пример #1
0
def transfer(puzzle, cleanup=True, skip_status_update=False):
    """
    Transfer the puzzle data from Redis to the database. If the cleanup flag is
    set the Redis data for the puzzle will be removed afterward.
    """
    current_app.logger.info("transferring puzzle: {0}".format(puzzle))
    cur = db.cursor()

    query = """select * from Puzzle where (id = :puzzle)"""
    (result, col_names) = rowify(
        cur.execute(query, {
            "puzzle": puzzle
        }).fetchall(), cur.description)
    if not result:
        # Most likely because of a database switch and forgot to run this script
        # between those actions.
        # TODO: Raise an error here and let the caller decide how to handle it.
        current_app.logger.warning(
            "Puzzle {} not in database. Skipping.".format(puzzle))
        return

    puzzle_data = result[0]

    puzzle_previous_status = puzzle_data["status"]
    if not skip_status_update:
        r = requests.patch(
            "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/".
            format(
                HOSTAPI=current_app.config["HOSTAPI"],
                PORTAPI=current_app.config["PORTAPI"],
                puzzle_id=puzzle_data["puzzle_id"],
            ),
            json={
                "status": MAINTENANCE,
            },
        )
        if r.status_code != 200:
            # TODO: Raise an error here and let the caller decide how to handle it.
            current_app.logger.warning(
                "Puzzle details api error when setting status to maintenance {}"
                .format(puzzle_data["puzzle_id"]))
            return

    (all_pieces, col_names) = rowify(
        cur.execute(read_query_file("select_all_piece_props_for_puzzle.sql"), {
            "puzzle": puzzle
        }).fetchall(),
        cur.description,
    )

    # Save the redis data to the db if it has changed
    changed_pieces = []
    pcstacked = set(map(int, redis_connection.smembers(f"pcstacked:{puzzle}")))
    pcfixed = set(map(int, redis_connection.smembers(f"pcfixed:{puzzle}")))
    for piece in all_pieces:
        has_changes = False
        pieceFromRedis = redis_connection.hgetall("pc:{puzzle}:{id}".format(
            puzzle=puzzle, id=piece["id"]))

        # The redis data may be empty so skip updating the db
        if len(pieceFromRedis) == 0:
            continue

        # Compare redis data with db for any changes
        for (prop, colname) in [
            ("x", "x"),
            ("y", "y"),
            ("r", "r"),
            ("g", "parent"),
            ("", "status"),
        ]:
            if colname == "status":
                if piece["id"] in pcstacked:
                    redis_piece_prop = 2
                if piece["id"] in pcfixed:
                    redis_piece_prop = 1
            else:
                redis_piece_prop = pieceFromRedis.get(prop)
                redis_piece_prop = (int(redis_piece_prop) if isinstance(
                    redis_piece_prop, str) else redis_piece_prop)
            if redis_piece_prop != piece[colname]:
                current_app.logger.debug("{} has {} changes. {} != {}".format(
                    piece["id"], colname, redis_piece_prop, piece[colname]))
                piece[colname] = redis_piece_prop
                has_changes = True

        if has_changes:
            changed_pieces.append(piece)

    if len(changed_pieces) != 0:
        r = requests.patch(
            "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/pieces/".
            format(
                HOSTAPI=current_app.config["HOSTAPI"],
                PORTAPI=current_app.config["PORTAPI"],
                puzzle_id=puzzle_data["puzzle_id"],
            ),
            json={"piece_properties": changed_pieces},
        )
        if r.status_code != 200:
            raise Exception(
                "Puzzle pieces api error. Failed to patch pieces. {}".format(
                    r))

    if cleanup:
        deletePieceDataFromRedis(redis_connection, puzzle, all_pieces)

    cur.close()

    if not skip_status_update:
        r = requests.patch(
            "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/".
            format(
                HOSTAPI=current_app.config["HOSTAPI"],
                PORTAPI=current_app.config["PORTAPI"],
                puzzle_id=puzzle_data["puzzle_id"],
            ),
            json={
                "status": puzzle_previous_status,
            },
        )
        if r.status_code != 200:
            # TODO: Raise an error here and let the caller decide how to handle it.
            current_app.logger.warning("Puzzle details api error")
            return
Пример #2
0
    def do_task(self):
        super().do_task()
        made_change = False

        cur = db.cursor()
        for (low, high) in SKILL_LEVEL_RANGES:
            in_queue_puzzle_count = cur.execute(
                read_query_file("get_in_queue_puzzle_count.sql"),
                {
                    "low": low,
                    "high": high
                },
            ).fetchone()[0]
            if in_queue_puzzle_count <= self.minimum_count:
                (result, col_names) = rowify(
                    cur.execute(
                        read_query_file("select_random_puzzle_to_rebuild.sql"),
                        {
                            "status": COMPLETED,
                            "low": low,
                            "high": high
                        },
                    ).fetchall(),
                    cur.description,
                )
                if result:
                    for completed_puzzle in result:
                        puzzle = completed_puzzle["id"]

                        current_app.logger.debug(
                            "found puzzle {id}".format(**completed_puzzle))
                        # Update puzzle status to be REBUILD and change the piece count
                        pieces = random.randint(
                            max(
                                int(current_app.config["MINIMUM_PIECE_COUNT"]),
                                max(completed_puzzle["pieces"] - 400, low),
                            ),
                            min(high - 1, completed_puzzle["pieces"] + 400),
                        )
                        r = requests.patch(
                            "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                            .format(
                                HOSTAPI=current_app.config["HOSTAPI"],
                                PORTAPI=current_app.config["PORTAPI"],
                                puzzle_id=completed_puzzle["puzzle_id"],
                            ),
                            json={
                                "status": REBUILD,
                                "pieces": pieces,
                                "queue": QUEUE_END_OF_LINE,
                            },
                        )
                        if r.status_code != 200:
                            current_app.logger.warning(
                                "Puzzle details api error. Could not set puzzle status to rebuild. Skipping {puzzle_id}"
                                .format(**completed_puzzle))
                            continue
                        completed_puzzle["status"] = REBUILD
                        completed_puzzle["pieces"] = pieces

                        # Delete any piece data from redis since it is no longer needed.
                        query_select_all_pieces_for_puzzle = (
                            """select * from Piece where (puzzle = :puzzle)""")
                        (all_pieces, col_names) = rowify(
                            cur.execute(query_select_all_pieces_for_puzzle, {
                                "puzzle": puzzle
                            }).fetchall(),
                            cur.description,
                        )
                        deletePieceDataFromRedis(redis_connection, puzzle,
                                                 all_pieces)

                        job = self.queue.enqueue_call(
                            func="api.jobs.pieceRenderer.render",
                            args=([completed_puzzle]),
                            result_ttl=0,
                            timeout="24h",
                        )

                        archive_and_clear(puzzle)
                    made_change = True

        cur.close()
        if made_change:
            self.log_task()
Пример #3
0
    def do_task(self):
        super().do_task()
        made_change = False

        cur = db.cursor()
        in_queue_puzzles_in_piece_groups = current_app.config[
            "MINIMUM_IN_QUEUE_PUZZLES_IN_PIECE_GROUPS"].copy()
        in_queue_puzzles_in_piece_groups.reverse()
        for (low, high, minimum_count) in map(
                lambda x: (x[0], x[1], in_queue_puzzles_in_piece_groups.pop()),
                current_app.config["SKILL_LEVEL_RANGES"],
        ):
            if minimum_count == 0:
                continue
            in_queue_puzzle_count = cur.execute(
                read_query_file("get_in_queue_puzzle_count.sql"),
                {
                    "low": low,
                    "high": high
                },
            ).fetchone()[0]
            if in_queue_puzzle_count <= minimum_count:
                (result, col_names) = rowify(
                    cur.execute(
                        read_query_file("select_random_puzzle_to_rebuild.sql"),
                        {
                            "status": COMPLETED,
                            "low": max(0, low - 500),
                            "high": high + 500,
                        },
                    ).fetchall(),
                    cur.description,
                )
                if not result:
                    # try again with wider range of puzzle piece counts
                    (result, col_names) = rowify(
                        cur.execute(
                            read_query_file(
                                "select_random_puzzle_to_rebuild.sql"),
                            {
                                "status": COMPLETED,
                                "low": max(0, low - 2000),
                                "high": high + 2000,
                            },
                        ).fetchall(),
                        cur.description,
                    )
                if result:
                    for completed_puzzle in result:
                        puzzle = completed_puzzle["id"]

                        current_app.logger.debug(
                            "found puzzle {id}".format(**completed_puzzle))
                        # Update puzzle status to be REBUILD and change the piece count
                        low_range = max(
                            int(current_app.config["MINIMUM_PIECE_COUNT"]),
                            min(
                                max(low, (high - 400)),
                                max(low, (completed_puzzle["pieces"] - 400)),
                            ),
                        )
                        high_range = min(
                            high,
                            max((low + 400),
                                (completed_puzzle["pieces"] + 400)))
                        pieces = random.randint(low_range, high_range)
                        sleep(API_REQUESTS_LIMIT_RATE)
                        r = requests.patch(
                            "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                            .format(
                                HOSTAPI=current_app.config["HOSTAPI"],
                                PORTAPI=current_app.config["PORTAPI"],
                                puzzle_id=completed_puzzle["puzzle_id"],
                            ),
                            json={
                                "status": REBUILD,
                                "pieces": pieces,
                                "queue": QUEUE_END_OF_LINE,
                            },
                        )
                        if r.status_code != 200:
                            current_app.logger.warning(
                                "Puzzle details api error. Could not set puzzle status to rebuild. Skipping {puzzle_id}"
                                .format(**completed_puzzle))
                            continue
                        completed_puzzle["status"] = REBUILD
                        completed_puzzle["pieces"] = pieces

                        # Delete any piece data from redis since it is no longer needed.
                        query_select_all_pieces_for_puzzle = (
                            """select * from Piece where (puzzle = :puzzle)""")
                        (all_pieces, col_names) = rowify(
                            cur.execute(query_select_all_pieces_for_puzzle, {
                                "puzzle": puzzle
                            }).fetchall(),
                            cur.description,
                        )
                        deletePieceDataFromRedis(redis_connection, puzzle,
                                                 all_pieces)

                        job = self.queue.enqueue(
                            "api.jobs.pieceRenderer.render",
                            [completed_puzzle],
                            result_ttl=0,
                            job_timeout="24h",
                        )

                        archive_and_clear(puzzle)
                    made_change = True

        cur.close()
        if made_change:
            self.log_task()
Пример #4
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        puzzle_id = args.get("puzzle_id")
        if not puzzle_id:
            abort(400)

        # Check pieces arg
        try:
            pieces = int(
                args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"]))
        except ValueError as err:
            abort(400)
        if pieces < current_app.config["MINIMUM_PIECE_COUNT"]:
            abort(400)

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        cur = db.cursor()
        result = cur.execute(
            fetch_query_string(
                "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": COMPLETED
            },
        ).fetchall()
        if not result:
            # Puzzle does not exist or is not completed status.
            # Reload the page as the status may have been changed.
            cur.close()
            return redirect(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))

        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]
        puzzle = puzzleData["id"]

        userCanRebuildPuzzle = cur.execute(
            fetch_query_string("select-user-rebuild-puzzle-prereq.sql"),
            {
                "user": user,
                "puzzle": puzzle,
                "pieces": pieces
            },
        ).fetchall()
        if not userCanRebuildPuzzle:
            cur.close()
            abort(400)

        original_puzzle_id = puzzleData["original_puzzle_id"]
        # Get the adjusted piece count depending on the size of the original and
        # the minimum piece size.
        original_puzzle_dir = os.path.join(
            current_app.config["PUZZLE_RESOURCES"], original_puzzle_id)
        # TODO: get path of original.jpg via the PuzzleFile query
        # TODO: use requests.get to get original.jpg and run in another thread
        imagefile = os.path.join(original_puzzle_dir, "original.jpg")
        im = Image.open(imagefile)
        (width, height) = im.size
        im.close()
        max_pieces_that_will_fit = int((old_div(width, MIN_PIECE_SIZE)) *
                                       (old_div(height, MIN_PIECE_SIZE)))

        # The user points for rebuilding the puzzle is decreased by the piece
        # count for the puzzle. Use at least minimum piece count (20) points for
        # smaller puzzles.  Players that own a puzzle instance do not decrease
        # any points (dots) if the puzzle is complete.
        point_cost = max(
            current_app.config["MINIMUM_PIECE_COUNT"],
            min(
                max_pieces_that_will_fit,
                pieces,
                current_app.config["MAX_POINT_COST_FOR_REBUILDING"],
            ),
        )
        if not (puzzleData["owner"] == user and puzzleData["puzzle_id"]
                == puzzleData["original_puzzle_id"]):
            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {
                    "user": user,
                    "points": point_cost
                },
            )

        # Update puzzle status to be REBUILD and change the piece count
        cur.execute(
            fetch_query_string("update_status_puzzle_for_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": REBUILD,
                "pieces": pieces,
                "queue": QUEUE_REBUILD,
            },
        )
        puzzleData["status"] = REBUILD
        puzzleData["pieces"] = pieces

        db.commit()

        # Delete any piece data from redis since it is no longer needed.
        (all_pieces, col_names) = rowify(
            cur.execute(
                fetch_query_string("select_all_piece_ids_for_puzzle.sql"),
                {
                    "puzzle": puzzle
                },
            ).fetchall(),
            cur.description,
        )
        cur.close()
        deletePieceDataFromRedis(redis_connection, puzzle, all_pieces)

        job = current_app.createqueue.enqueue_call(
            func="api.jobs.pieceRenderer.render",
            args=([puzzleData]),
            result_ttl=0,
            timeout="24h",
        )

        job = current_app.cleanupqueue.enqueue_call(
            func="api.jobs.timeline_archive.archive_and_clear",
            kwargs=({
                "puzzle": puzzle
            }),
            result_ttl=0,
            timeout="24h",
        )

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))
Пример #5
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        puzzle_id = args.get("puzzle_id")
        if not puzzle_id:
            abort(400)

        # Check pieces arg
        try:
            pieces = int(
                args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"]))
        except ValueError as err:
            abort(400)
        if pieces < current_app.config["MINIMUM_PIECE_COUNT"]:
            abort(400)

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        cur = db.cursor()
        result = cur.execute(
            fetch_query_string(
                "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": COMPLETED
            },
        ).fetchall()
        if not result:
            # Puzzle does not exist or is not completed status.
            # Reload the page as the status may have been changed.
            cur.close()
            return redirect(
                "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))

        (result, col_names) = rowify(result, cur.description)
        puzzleData = result[0]
        puzzle = puzzleData["id"]

        userCanRebuildPuzzle = cur.execute(
            fetch_query_string("select-user-rebuild-puzzle-prereq.sql"),
            {
                "user": user,
                "puzzle": puzzle,
                "pieces": pieces
            },
        ).fetchall()
        if not userCanRebuildPuzzle:
            cur.close()
            abort(400)

        if (puzzleData["permission"] == PRIVATE and
                puzzleData["original_puzzle_id"] == puzzleData["puzzle_id"]):
            current_app.logger.warning(
                "Original puzzles that are private can not be rebuilt")
            cur.close()
            abort(400)

        # The user points for rebuilding the puzzle is decreased by the piece
        # count for the puzzle. Use at least minimum piece count (20) points for
        # smaller puzzles.  Players that own a puzzle instance do not decrease
        # any points (dots) if the puzzle is complete.
        point_cost = max(
            current_app.config["MINIMUM_PIECE_COUNT"],
            min(
                pieces,
                current_app.config["MAX_POINT_COST_FOR_REBUILDING"],
            ),
        )
        if not (puzzleData["owner"] == user and puzzleData["puzzle_id"]
                == puzzleData["original_puzzle_id"]):
            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {
                    "user": user,
                    "points": point_cost
                },
            )

        # Update puzzle status to be REBUILD and change the piece count
        cur.execute(
            fetch_query_string("update_status_puzzle_for_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id,
                "status": REBUILD,
                "pieces": pieces,
                "queue": QUEUE_REBUILD,
            },
        )
        puzzleData["status"] = REBUILD
        puzzleData["pieces"] = pieces

        db.commit()

        # Delete any piece data from redis since it is no longer needed.
        (all_pieces, col_names) = rowify(
            cur.execute(
                fetch_query_string("select_all_piece_ids_for_puzzle.sql"),
                {
                    "puzzle": puzzle
                },
            ).fetchall(),
            cur.description,
        )
        cur.close()
        deletePieceDataFromRedis(redis_connection, puzzle, all_pieces)

        delete_puzzle_resources(
            puzzle_id,
            is_local_resource=not puzzleData["preview_full"].startswith("http")
            and not puzzleData["preview_full"].startswith("//"),
            exclude_regex=r"(original|preview_full).([^.]+\.)?jpg")

        job = current_app.createqueue.enqueue(
            "api.jobs.pieceRenderer.render",
            [puzzleData],
            result_ttl=0,
            job_timeout="24h",
        )

        job = current_app.cleanupqueue.enqueue(
            "api.jobs.timeline_archive.archive_and_clear",
            puzzle,
            result_ttl=0,
            job_timeout="24h",
        )

        purge_route_from_nginx_cache(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id),
            current_app.config.get("PURGEURLLIST"),
        )

        return redirect(
            "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))